mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
test(core): add benchmark for defer runtime logic (#52222)
This commit adds a benchmark for `@defer` runtime logic and uses `@if` as a baseline. PR Close #52222
This commit is contained in:
parent
c2560d05f2
commit
c76cac25eb
14 changed files with 580 additions and 0 deletions
39
modules/benchmarks/src/defer/BUILD.bazel
Normal file
39
modules/benchmarks/src/defer/BUILD.bazel
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
load("//tools:defaults.bzl", "ng_module", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ng_module(
|
||||
name = "shared_lib",
|
||||
srcs = [
|
||||
"init.ts",
|
||||
"util.ts",
|
||||
],
|
||||
tsconfig = "//modules/benchmarks:tsconfig-build.json",
|
||||
deps = [
|
||||
"//modules/benchmarks/src:util_lib",
|
||||
"//packages/core",
|
||||
"//packages/platform-browser",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "perf_tests_lib",
|
||||
testonly = 1,
|
||||
srcs = ["defer.perf-spec.ts"],
|
||||
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
|
||||
deps = [
|
||||
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
|
||||
"@npm//protractor",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "e2e_tests_lib",
|
||||
testonly = 1,
|
||||
srcs = ["defer.e2e-spec.ts"],
|
||||
tsconfig = "//modules/benchmarks:tsconfig-e2e.json",
|
||||
deps = [
|
||||
"@npm//@angular/build-tooling/bazel/benchmark/driver-utilities",
|
||||
"@npm//protractor",
|
||||
],
|
||||
)
|
||||
10
modules/benchmarks/src/defer/README.md
Normal file
10
modules/benchmarks/src/defer/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Defer benchmark
|
||||
|
||||
This folder contains defer benchmark that tests the process of `@defer` block creation.
|
||||
|
||||
There are 2 folders in this benchmark:
|
||||
|
||||
* `baseline` - renders a component using an `@if` condition, we use it as a baseline
|
||||
* `main` - the same code as the `baseline`, but instead of the `@if`, we use `@defer` to compare defer blocks against conditionals
|
||||
|
||||
The benchmarks are based on `largetable` benchmarks.
|
||||
55
modules/benchmarks/src/defer/baseline/BUILD.bazel
Normal file
55
modules/benchmarks/src/defer/baseline/BUILD.bazel
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
|
||||
load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
|
||||
load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
|
||||
|
||||
package(default_visibility = ["//modules/benchmarks:__subpackages__"])
|
||||
|
||||
ng_module(
|
||||
name = "main",
|
||||
srcs = glob(["*.ts"]),
|
||||
tsconfig = "//modules/benchmarks:tsconfig-build.json",
|
||||
deps = [
|
||||
"//modules/benchmarks/src:util_lib",
|
||||
"//modules/benchmarks/src/defer:shared_lib",
|
||||
"//packages/core",
|
||||
"//packages/platform-browser",
|
||||
],
|
||||
)
|
||||
|
||||
app_bundle(
|
||||
name = "bundle",
|
||||
entry_point = ":index.ts",
|
||||
deps = [
|
||||
":main",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
)
|
||||
|
||||
# The script needs to be called `app_bundle` for easier syncing into g3.
|
||||
genrule(
|
||||
name = "app_bundle",
|
||||
srcs = [":bundle.debug.min.js"],
|
||||
outs = ["app_bundle.js"],
|
||||
cmd = "cp $< $@",
|
||||
)
|
||||
|
||||
http_server(
|
||||
name = "prodserver",
|
||||
srcs = ["index.html"],
|
||||
deps = [
|
||||
":app_bundle",
|
||||
"//packages/zone.js/bundles:zone.umd.js",
|
||||
],
|
||||
)
|
||||
|
||||
benchmark_test(
|
||||
name = "perf",
|
||||
server = ":prodserver",
|
||||
deps = ["//modules/benchmarks/src/defer:perf_tests_lib"],
|
||||
)
|
||||
|
||||
e2e_test(
|
||||
name = "e2e",
|
||||
server = ":prodserver",
|
||||
deps = ["//modules/benchmarks/src/defer:e2e_tests_lib"],
|
||||
)
|
||||
55
modules/benchmarks/src/defer/baseline/app.component.ts
Normal file
55
modules/benchmarks/src/defer/baseline/app.component.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
|
||||
|
||||
import {TableCell} from '../util';
|
||||
|
||||
let trustedEmptyColor: SafeStyle;
|
||||
let trustedGreyColor: SafeStyle;
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<table>
|
||||
<tbody>
|
||||
@for (row of data; track $index) {
|
||||
<tr>
|
||||
@for (cell of row; track $index) {
|
||||
<td [style.backgroundColor]="getColor(cell.row)">
|
||||
@if (condition) {
|
||||
<!--
|
||||
Use static text in cells to avoid the need
|
||||
to run a new change detection cycle.
|
||||
-->
|
||||
Cell
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
})
|
||||
export class AppComponent {
|
||||
@Input() data: TableCell[][] = [];
|
||||
|
||||
condition = true;
|
||||
|
||||
constructor(sanitizer: DomSanitizer) {
|
||||
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white');
|
||||
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
|
||||
}
|
||||
|
||||
getColor(row: number) {
|
||||
return row % 2 ? trustedEmptyColor : trustedGreyColor;
|
||||
}
|
||||
}
|
||||
39
modules/benchmarks/src/defer/baseline/index.html
Normal file
39
modules/benchmarks/src/defer/baseline/index.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- Prevent the browser from requesting any favicon. -->
|
||||
<link rel="icon" href="data:," />
|
||||
</head>
|
||||
<body>
|
||||
<h2>Params</h2>
|
||||
<form>
|
||||
Cols:
|
||||
<input type="number" id="cols" name="cols" value="" />
|
||||
<br />
|
||||
Rows:
|
||||
<input type="number" id="rows" name="rows" value="" />
|
||||
<br />
|
||||
<button>Apply</button>
|
||||
</form>
|
||||
|
||||
<h2>Defer Benchmark (baseline)</h2>
|
||||
<p>
|
||||
<button id="destroyDom">destroyDom</button>
|
||||
<button id="createDom">createDom</button>
|
||||
<button id="createDomProfile">profile createDom</button>
|
||||
<button id="updateDomProfile">profile updateDom</button>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<app id="root"></app>
|
||||
</div>
|
||||
|
||||
<!-- BEGIN-EXTERNAL -->
|
||||
<script src="/angular/packages/zone.js/bundles/zone.umd.js"></script>
|
||||
<!-- END-EXTERNAL -->
|
||||
|
||||
<!-- Needs to be named `app_bundle` for sync into Google. -->
|
||||
<script src="/app_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
modules/benchmarks/src/defer/baseline/index.ts
Normal file
21
modules/benchmarks/src/defer/baseline/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
|
||||
|
||||
import {init, syncUrlParamsToForm} from '../init';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
|
||||
syncUrlParamsToForm();
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideProtractorTestingSupport(),
|
||||
],
|
||||
}).then(init);
|
||||
24
modules/benchmarks/src/defer/defer.e2e-spec.ts
Normal file
24
modules/benchmarks/src/defer/defer.e2e-spec.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {openBrowser, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
|
||||
import {$} from 'protractor';
|
||||
|
||||
describe('defer benchmark', () => {
|
||||
afterEach(verifyNoBrowserErrors);
|
||||
|
||||
it(`should render the table`, async () => {
|
||||
openBrowser({
|
||||
url: '',
|
||||
ignoreBrowserSynchronization: true,
|
||||
params: [{name: 'cols', value: 5}, {name: 'rows', value: 5}],
|
||||
});
|
||||
await $('#createDom').click();
|
||||
expect($('#root').getText()).toContain('Cell');
|
||||
});
|
||||
});
|
||||
67
modules/benchmarks/src/defer/defer.perf-spec.ts
Normal file
67
modules/benchmarks/src/defer/defer.perf-spec.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {runBenchmark, verifyNoBrowserErrors} from '@angular/build-tooling/bazel/benchmark/driver-utilities';
|
||||
import {$} from 'protractor';
|
||||
|
||||
interface Worker {
|
||||
id: string;
|
||||
prepare?(): void;
|
||||
work(): void;
|
||||
}
|
||||
|
||||
const CreateWorker: Worker = {
|
||||
id: 'create',
|
||||
prepare: () => $('#destroyDom').click(),
|
||||
work: () => $('#createDom').click()
|
||||
};
|
||||
|
||||
const UpdateWorker: Worker = {
|
||||
id: 'update',
|
||||
prepare: () => {
|
||||
$('#createDom').click();
|
||||
},
|
||||
work: () => $('#createDom').click()
|
||||
};
|
||||
|
||||
// In order to make sure that we don't change the ids of the benchmarks, we need to
|
||||
// determine the current test package name from the Bazel target. This is necessary
|
||||
// because previous to the Bazel conversion, the benchmark test ids contained the test
|
||||
// name. e.g. "largeTable.ng2_switch.createDestroy". We determine the name of the
|
||||
// Bazel package where this test runs from the current test target. The Bazel target
|
||||
// looks like: "//modules/benchmarks/src/largetable/{pkg_name}:{target_name}".
|
||||
const testPackageName = process.env['BAZEL_TARGET']!.split(':')[0].split('/').pop();
|
||||
|
||||
describe('defer benchmark perf', () => {
|
||||
afterEach(verifyNoBrowserErrors);
|
||||
|
||||
[CreateWorker, UpdateWorker].forEach((worker) => {
|
||||
describe(worker.id, () => {
|
||||
it(`should run benchmark for ${testPackageName}`, async () => {
|
||||
await runTableBenchmark({
|
||||
id: `defer.${testPackageName}.${worker.id}`,
|
||||
url: '/',
|
||||
ignoreBrowserSynchronization: true,
|
||||
worker,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function runTableBenchmark(
|
||||
config: {id: string, url: string, ignoreBrowserSynchronization?: boolean, worker: Worker}) {
|
||||
return runBenchmark({
|
||||
id: config.id,
|
||||
url: config.url,
|
||||
ignoreBrowserSynchronization: config.ignoreBrowserSynchronization,
|
||||
params: [{name: 'cols', value: 40}, {name: 'rows', value: 200}],
|
||||
prepare: config.worker.prepare,
|
||||
work: config.worker.work
|
||||
});
|
||||
}
|
||||
52
modules/benchmarks/src/defer/init.ts
Normal file
52
modules/benchmarks/src/defer/init.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ApplicationRef} from '@angular/core';
|
||||
|
||||
import {bindAction, profile} from '../util';
|
||||
|
||||
import {buildTable, emptyTable, initTableUtils} from './util';
|
||||
|
||||
const DEFAULT_COLS_COUNT = '40';
|
||||
const DEFAULT_ROWS_COUNT = '200';
|
||||
|
||||
function getUrlParamValue(name: string): string|null {
|
||||
const url = new URL(document.location.href);
|
||||
return url.searchParams.get(name);
|
||||
}
|
||||
|
||||
export function syncUrlParamsToForm(): {cols: string, rows: string} {
|
||||
let cols = getUrlParamValue('cols') ?? DEFAULT_COLS_COUNT;
|
||||
let rows = getUrlParamValue('rows') ?? DEFAULT_ROWS_COUNT;
|
||||
(document.getElementById('cols') as HTMLInputElement).value = cols;
|
||||
(document.getElementById('rows') as HTMLInputElement).value = rows;
|
||||
return {cols, rows};
|
||||
}
|
||||
|
||||
export function init(appRef: ApplicationRef) {
|
||||
const table = appRef.components[0].instance;
|
||||
|
||||
function destroyDom() {
|
||||
table.data = emptyTable;
|
||||
appRef.tick();
|
||||
}
|
||||
|
||||
function createDom() {
|
||||
table.data = buildTable();
|
||||
appRef.tick();
|
||||
}
|
||||
|
||||
function noop() {}
|
||||
|
||||
initTableUtils();
|
||||
|
||||
bindAction('#destroyDom', destroyDom);
|
||||
bindAction('#createDom', createDom);
|
||||
bindAction('#createDomProfile', profile(createDom, destroyDom, 'create'));
|
||||
bindAction('#updateDomProfile', profile(createDom, noop, 'update'));
|
||||
}
|
||||
55
modules/benchmarks/src/defer/main/BUILD.bazel
Normal file
55
modules/benchmarks/src/defer/main/BUILD.bazel
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
load("//tools:defaults.bzl", "app_bundle", "http_server", "ng_module")
|
||||
load("@npm//@angular/build-tooling/bazel/benchmark/component_benchmark:benchmark_test.bzl", "benchmark_test")
|
||||
load("//modules/benchmarks:e2e_test.bzl", "e2e_test")
|
||||
|
||||
package(default_visibility = ["//modules/benchmarks:__subpackages__"])
|
||||
|
||||
ng_module(
|
||||
name = "main",
|
||||
srcs = glob(["*.ts"]),
|
||||
tsconfig = "//modules/benchmarks:tsconfig-build.json",
|
||||
deps = [
|
||||
"//modules/benchmarks/src:util_lib",
|
||||
"//modules/benchmarks/src/defer:shared_lib",
|
||||
"//packages/core",
|
||||
"//packages/platform-browser",
|
||||
],
|
||||
)
|
||||
|
||||
app_bundle(
|
||||
name = "bundle",
|
||||
entry_point = ":index.ts",
|
||||
deps = [
|
||||
":main",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
)
|
||||
|
||||
# The script needs to be called `app_bundle` for easier syncing into g3.
|
||||
genrule(
|
||||
name = "app_bundle",
|
||||
srcs = [":bundle.debug.min.js"],
|
||||
outs = ["app_bundle.js"],
|
||||
cmd = "cp $< $@",
|
||||
)
|
||||
|
||||
http_server(
|
||||
name = "prodserver",
|
||||
srcs = ["index.html"],
|
||||
deps = [
|
||||
":app_bundle",
|
||||
"//packages/zone.js/bundles:zone.umd.js",
|
||||
],
|
||||
)
|
||||
|
||||
benchmark_test(
|
||||
name = "perf",
|
||||
server = ":prodserver",
|
||||
deps = ["//modules/benchmarks/src/defer:perf_tests_lib"],
|
||||
)
|
||||
|
||||
e2e_test(
|
||||
name = "e2e",
|
||||
server = ":prodserver",
|
||||
deps = ["//modules/benchmarks/src/defer:e2e_tests_lib"],
|
||||
)
|
||||
55
modules/benchmarks/src/defer/main/app.component.ts
Normal file
55
modules/benchmarks/src/defer/main/app.component.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {DomSanitizer, SafeStyle} from '@angular/platform-browser';
|
||||
|
||||
import {TableCell} from '../util';
|
||||
|
||||
let trustedEmptyColor: SafeStyle;
|
||||
let trustedGreyColor: SafeStyle;
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app',
|
||||
template: `
|
||||
<table>
|
||||
<tbody>
|
||||
@for (row of data; track $index) {
|
||||
<tr>
|
||||
@for (cell of row; track $index) {
|
||||
<td [style.backgroundColor]="getColor(cell.row)">
|
||||
@defer (when condition; on immediate) {
|
||||
<!--
|
||||
Use static text in cells to avoid the need
|
||||
to run a new change detection cycle.
|
||||
-->
|
||||
Cell
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
})
|
||||
export class AppComponent {
|
||||
@Input() data: TableCell[][] = [];
|
||||
|
||||
condition = true;
|
||||
|
||||
constructor(sanitizer: DomSanitizer) {
|
||||
trustedEmptyColor = sanitizer.bypassSecurityTrustStyle('white');
|
||||
trustedGreyColor = sanitizer.bypassSecurityTrustStyle('grey');
|
||||
}
|
||||
|
||||
getColor(row: number) {
|
||||
return row % 2 ? trustedEmptyColor : trustedGreyColor;
|
||||
}
|
||||
}
|
||||
39
modules/benchmarks/src/defer/main/index.html
Normal file
39
modules/benchmarks/src/defer/main/index.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<!-- Prevent the browser from requesting any favicon. -->
|
||||
<link rel="icon" href="data:," />
|
||||
</head>
|
||||
<body>
|
||||
<h2>Params</h2>
|
||||
<form>
|
||||
Cols:
|
||||
<input type="number" id="cols" name="cols" value="" />
|
||||
<br />
|
||||
Rows:
|
||||
<input type="number" id="rows" name="rows" value="" />
|
||||
<br />
|
||||
<button>Apply</button>
|
||||
</form>
|
||||
|
||||
<h2>Defer Benchmark (main)</h2>
|
||||
<p>
|
||||
<button id="destroyDom">destroyDom</button>
|
||||
<button id="createDom">createDom</button>
|
||||
<button id="createDomProfile">profile createDom</button>
|
||||
<button id="updateDomProfile">profile updateDom</button>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<app id="root"></app>
|
||||
</div>
|
||||
|
||||
<!-- BEGIN-EXTERNAL -->
|
||||
<script src="/angular/packages/zone.js/bundles/zone.umd.js"></script>
|
||||
<!-- END-EXTERNAL -->
|
||||
|
||||
<!-- Needs to be named `app_bundle` for sync into Google. -->
|
||||
<script src="/app_bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
21
modules/benchmarks/src/defer/main/index.ts
Normal file
21
modules/benchmarks/src/defer/main/index.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
|
||||
|
||||
import {init, syncUrlParamsToForm} from '../init';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
|
||||
syncUrlParamsToForm();
|
||||
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideProtractorTestingSupport(),
|
||||
],
|
||||
}).then(init);
|
||||
48
modules/benchmarks/src/defer/util.ts
Normal file
48
modules/benchmarks/src/defer/util.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {getIntParameter} from '../util';
|
||||
|
||||
export class TableCell {
|
||||
constructor(public row: number, public col: number, public value: string) {}
|
||||
}
|
||||
|
||||
let tableCreateCount: number;
|
||||
let maxRow: number;
|
||||
let maxCol: number;
|
||||
let numberData: TableCell[][];
|
||||
let charData: TableCell[][];
|
||||
|
||||
export function initTableUtils() {
|
||||
maxRow = getIntParameter('rows');
|
||||
maxCol = getIntParameter('cols');
|
||||
tableCreateCount = 0;
|
||||
numberData = [];
|
||||
charData = [];
|
||||
for (let r = 0; r < maxRow; r++) {
|
||||
const numberRow: TableCell[] = [];
|
||||
numberData.push(numberRow);
|
||||
const charRow: TableCell[] = [];
|
||||
charData.push(charRow);
|
||||
for (let c = 0; c < maxCol; c++) {
|
||||
numberRow.push(new TableCell(r, c, `${c}/${r}`));
|
||||
charRow.push(new TableCell(r, c, `${charValue(c)}/${charValue(r)}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function charValue(i: number): string {
|
||||
return String.fromCharCode('A'.charCodeAt(0) + (i % 26));
|
||||
}
|
||||
|
||||
export const emptyTable: TableCell[][] = [];
|
||||
|
||||
export function buildTable(): TableCell[][] {
|
||||
tableCreateCount++;
|
||||
return tableCreateCount % 2 ? numberData : charData;
|
||||
}
|
||||
Loading…
Reference in a new issue