overtake 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/cli.js CHANGED
@@ -83,6 +83,7 @@ if (opts['compare-baseline']) {
83
83
  }
84
84
  }
85
85
  const files = new Set((await Promise.all(patterns.map((pattern) => Array.fromAsync(glob(pattern, { cwd: process.cwd() })).catch(() => [])))).flat());
86
+ const allBaselineResults = {};
86
87
  for (const file of files) {
87
88
  const stats = await stat(file).catch(() => false);
88
89
  if (stats && stats.isFile()) {
@@ -95,16 +96,28 @@ for (const file of files) {
95
96
  instance = Benchmark.create(...args);
96
97
  return instance;
97
98
  };
98
- await import(identifier);
99
+ try {
100
+ await import(identifier);
101
+ }
102
+ catch (e) {
103
+ console.error(`Error loading ${file}: ${e instanceof Error ? e.message : String(e)}`);
104
+ continue;
105
+ }
99
106
  if (instance) {
100
- const reports = await instance.execute({
101
- ...executeOptions,
102
- [BENCHMARK_URL]: identifier,
103
- });
107
+ let reports;
108
+ try {
109
+ reports = await instance.execute({
110
+ ...executeOptions,
111
+ [BENCHMARK_URL]: identifier,
112
+ });
113
+ }
114
+ catch (e) {
115
+ console.error(`Error executing ${file}: ${e instanceof Error ? e.message : String(e)}`);
116
+ continue;
117
+ }
104
118
  if (opts['save-baseline']) {
105
- const baselineData = reportsToBaseline(reports);
106
- await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
107
- console.log(`Baseline saved to: ${opts['save-baseline']}`);
119
+ const bd = reportsToBaseline(reports);
120
+ Object.assign(allBaselineResults, bd.results);
108
121
  }
109
122
  if (baseline) {
110
123
  printComparisonReports(reports, baseline);
@@ -133,3 +146,12 @@ for (const file of files) {
133
146
  }
134
147
  }
135
148
  }
149
+ if (opts['save-baseline'] && Object.keys(allBaselineResults).length > 0) {
150
+ const baselineData = {
151
+ version: 1,
152
+ timestamp: new Date().toISOString(),
153
+ results: allBaselineResults,
154
+ };
155
+ await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
156
+ console.log(`Baseline saved to: ${opts['save-baseline']}`);
157
+ }
package/build/executor.js CHANGED
@@ -2,7 +2,7 @@ import { Worker } from 'node:worker_threads';
2
2
  import { once } from 'node:events';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { createReport, computeStats, Report } from './reporter.js';
5
- import { cmp, assertNoClosure } from './utils.js';
5
+ import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
6
6
  import { Control, CONTROL_SLOTS, COMPLETE_VALUE, } from './types.js';
7
7
  const BENCHMARK_URL = Symbol.for('overtake.benchmarkUrl');
8
8
  export const createExecutor = (options) => {
@@ -10,6 +10,7 @@ export const createExecutor = (options) => {
10
10
  const benchmarkUrl = options[BENCHMARK_URL];
11
11
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
12
12
  const pending = [];
13
+ const activeWorkers = new Set();
13
14
  let running = 0;
14
15
  const schedule = async (task) => {
15
16
  running++;
@@ -33,11 +34,11 @@ export const createExecutor = (options) => {
33
34
  });
34
35
  };
35
36
  const runTask = async ({ id, setup, teardown, pre, run, post, data }) => {
36
- const setupCode = setup?.toString();
37
- const teardownCode = teardown?.toString();
38
- const preCode = pre?.toString();
39
- const runCode = run.toString();
40
- const postCode = post?.toString();
37
+ const setupCode = setup ? normalizeFunction(setup.toString()) : undefined;
38
+ const teardownCode = teardown ? normalizeFunction(teardown.toString()) : undefined;
39
+ const preCode = pre ? normalizeFunction(pre.toString()) : undefined;
40
+ const runCode = normalizeFunction(run.toString());
41
+ const postCode = post ? normalizeFunction(post.toString()) : undefined;
41
42
  if (setupCode)
42
43
  assertNoClosure(setupCode, 'setup');
43
44
  if (teardownCode)
@@ -69,6 +70,7 @@ export const createExecutor = (options) => {
69
70
  const worker = new Worker(workerFile, {
70
71
  workerData,
71
72
  });
73
+ activeWorkers.add(worker);
72
74
  const control = new Int32Array(controlSAB);
73
75
  let progressIntervalId;
74
76
  if (onProgress && id) {
@@ -96,6 +98,7 @@ export const createExecutor = (options) => {
96
98
  clearInterval(progressIntervalId);
97
99
  workerError = err instanceof Error ? err.message : String(err);
98
100
  }
101
+ activeWorkers.delete(worker);
99
102
  const count = control[Control.INDEX];
100
103
  const heapUsedKB = control[Control.HEAP_USED];
101
104
  const durations = new BigUint64Array(durationsSAB).slice(0, count).sort(cmp);
@@ -122,5 +125,15 @@ export const createExecutor = (options) => {
122
125
  ]);
123
126
  return Object.fromEntries(report);
124
127
  };
125
- return { pushAsync, kill() { } };
128
+ return {
129
+ pushAsync,
130
+ kill() {
131
+ for (const w of activeWorkers)
132
+ w.terminate();
133
+ activeWorkers.clear();
134
+ for (const p of pending)
135
+ p.reject(new Error('Executor killed'));
136
+ pending.length = 0;
137
+ },
138
+ };
126
139
  };
package/build/runner.js CHANGED
@@ -475,7 +475,6 @@ export const benchmark = async ({ setup, teardown, pre, run: runRaw, post, data,
475
475
  const durationNumber = Number(sampleDuration);
476
476
  if (!disableFiltering) {
477
477
  const { median, iqr } = medianAndIqr(outlierWindow);
478
- pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
479
478
  const maxAllowed = median + OUTLIER_IQR_MULTIPLIER * iqr || Number.POSITIVE_INFINITY;
480
479
  if (outlierWindow.length >= 8 && durationNumber > maxAllowed && durationNumber - median > OUTLIER_ABS_THRESHOLD) {
481
480
  skipped++;
@@ -487,9 +486,7 @@ export const benchmark = async ({ setup, teardown, pre, run: runRaw, post, data,
487
486
  continue;
488
487
  }
489
488
  }
490
- else {
491
- pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
492
- }
489
+ pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
493
490
  durations[i++] = sampleDuration;
494
491
  const deltaS = sampleDuration * WELFORD_SCALE - meanS;
495
492
  meanS += deltaS / BigInt(i);
package/build/utils.d.ts CHANGED
@@ -4,4 +4,5 @@ export declare const cmp: (a: bigint | number, b: bigint | number) => number;
4
4
  export declare const max: (a: bigint, b: bigint) => bigint;
5
5
  export declare function div(a: bigint, b: bigint, decimals?: number): string;
6
6
  export declare function divs(a: bigint, b: bigint, scale: bigint): bigint;
7
+ export declare function normalizeFunction(code: string): string;
7
8
  export declare function assertNoClosure(code: string, name: string): void;
package/build/utils.js CHANGED
@@ -44,11 +44,14 @@ export const max = (a, b) => {
44
44
  export function div(a, b, decimals = 2) {
45
45
  if (b === 0n)
46
46
  throw new RangeError('Division by zero');
47
+ const neg = (a < 0n) !== (b < 0n);
48
+ const absA = a < 0n ? -a : a;
49
+ const absB = b < 0n ? -b : b;
47
50
  const scale = 10n ** BigInt(decimals);
48
- const scaled = (a * scale) / b;
51
+ const scaled = (absA * scale) / absB;
49
52
  const intPart = scaled / scale;
50
53
  const fracPart = scaled % scale;
51
- return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
54
+ return `${neg ? '-' : ''}${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
52
55
  }
53
56
  export function divs(a, b, scale) {
54
57
  if (b === 0n)
@@ -57,22 +60,75 @@ export function divs(a, b, scale) {
57
60
  }
58
61
  const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
59
62
  KNOWN_GLOBALS.add('arguments');
60
- function collectUnresolved(node, result) {
63
+ let _unresolvedCtxt;
64
+ function findIdentifierCtxt(node, name) {
65
+ if (!node || typeof node !== 'object')
66
+ return undefined;
67
+ if (Array.isArray(node)) {
68
+ for (const item of node) {
69
+ const r = findIdentifierCtxt(item, name);
70
+ if (r !== undefined)
71
+ return r;
72
+ }
73
+ return undefined;
74
+ }
75
+ const obj = node;
76
+ if (obj.type === 'Identifier' && obj.value === name && typeof obj.ctxt === 'number') {
77
+ return obj.ctxt;
78
+ }
79
+ for (const key of Object.keys(obj)) {
80
+ if (key === 'span')
81
+ continue;
82
+ const r = findIdentifierCtxt(obj[key], name);
83
+ if (r !== undefined)
84
+ return r;
85
+ }
86
+ return undefined;
87
+ }
88
+ function probeUnresolvedCtxt() {
89
+ if (_unresolvedCtxt !== undefined)
90
+ return _unresolvedCtxt;
91
+ try {
92
+ const ast = parseSync('var _ = () => __PROBE__', { syntax: 'ecmascript', target: 'esnext' });
93
+ _unresolvedCtxt = findIdentifierCtxt(ast, '__PROBE__') ?? 1;
94
+ }
95
+ catch {
96
+ _unresolvedCtxt = 1;
97
+ }
98
+ return _unresolvedCtxt;
99
+ }
100
+ function collectUnresolved(node, ctxt, result) {
61
101
  if (!node || typeof node !== 'object')
62
102
  return;
63
103
  if (Array.isArray(node)) {
64
104
  for (const item of node)
65
- collectUnresolved(item, result);
105
+ collectUnresolved(item, ctxt, result);
66
106
  return;
67
107
  }
68
108
  const obj = node;
69
- if (obj.type === 'Identifier' && obj.ctxt === 1 && typeof obj.value === 'string') {
109
+ if (obj.type === 'Identifier' && obj.ctxt === ctxt && typeof obj.value === 'string') {
70
110
  result.add(obj.value);
71
111
  }
72
112
  for (const key of Object.keys(obj)) {
73
113
  if (key === 'span')
74
114
  continue;
75
- collectUnresolved(obj[key], result);
115
+ collectUnresolved(obj[key], ctxt, result);
116
+ }
117
+ }
118
+ export function normalizeFunction(code) {
119
+ try {
120
+ parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
121
+ return code;
122
+ }
123
+ catch {
124
+ const normalized = code.startsWith('async ') ? `async function ${code.slice(6)}` : `function ${code}`;
125
+ try {
126
+ parseSync(`var __fn = ${normalized}`, { syntax: 'ecmascript', target: 'esnext' });
127
+ return normalized;
128
+ }
129
+ catch {
130
+ return code;
131
+ }
76
132
  }
77
133
  }
78
134
  export function assertNoClosure(code, name) {
@@ -83,8 +139,9 @@ export function assertNoClosure(code, name) {
83
139
  catch {
84
140
  return;
85
141
  }
142
+ const unresolvedCtxt = probeUnresolvedCtxt();
86
143
  const unresolved = new Set();
87
- collectUnresolved(ast, unresolved);
144
+ collectUnresolved(ast, unresolvedCtxt, unresolved);
88
145
  for (const g of KNOWN_GLOBALS)
89
146
  unresolved.delete(g);
90
147
  if (unresolved.size === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "overtake",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "NodeJS performance benchmark",
5
5
  "type": "module",
6
6
  "types": "build/index.d.ts",
package/src/cli.ts CHANGED
@@ -105,6 +105,8 @@ if (opts['compare-baseline']) {
105
105
 
106
106
  const files = new Set((await Promise.all(patterns.map((pattern) => Array.fromAsync(glob(pattern, { cwd: process.cwd() })).catch(() => [] as string[])))).flat());
107
107
 
108
+ const allBaselineResults: Record<string, Record<string, number>> = {};
109
+
108
110
  for (const file of files) {
109
111
  const stats = await stat(file).catch(() => false as const);
110
112
  if (stats && stats.isFile()) {
@@ -117,18 +119,28 @@ for (const file of files) {
117
119
  instance = Benchmark.create(...args);
118
120
  return instance;
119
121
  };
120
- await import(identifier);
122
+ try {
123
+ await import(identifier);
124
+ } catch (e) {
125
+ console.error(`Error loading ${file}: ${e instanceof Error ? e.message : String(e)}`);
126
+ continue;
127
+ }
121
128
 
122
129
  if (instance) {
123
- const reports = await instance.execute({
124
- ...executeOptions,
125
- [BENCHMARK_URL]: identifier,
126
- } as typeof executeOptions);
130
+ let reports;
131
+ try {
132
+ reports = await instance.execute({
133
+ ...executeOptions,
134
+ [BENCHMARK_URL]: identifier,
135
+ } as typeof executeOptions);
136
+ } catch (e) {
137
+ console.error(`Error executing ${file}: ${e instanceof Error ? e.message : String(e)}`);
138
+ continue;
139
+ }
127
140
 
128
141
  if (opts['save-baseline']) {
129
- const baselineData = reportsToBaseline(reports);
130
- await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
131
- console.log(`Baseline saved to: ${opts['save-baseline']}`);
142
+ const bd = reportsToBaseline(reports);
143
+ Object.assign(allBaselineResults, bd.results);
132
144
  }
133
145
 
134
146
  if (baseline) {
@@ -157,3 +169,13 @@ for (const file of files) {
157
169
  }
158
170
  }
159
171
  }
172
+
173
+ if (opts['save-baseline'] && Object.keys(allBaselineResults).length > 0) {
174
+ const baselineData: BaselineData = {
175
+ version: 1,
176
+ timestamp: new Date().toISOString(),
177
+ results: allBaselineResults,
178
+ };
179
+ await writeFile(opts['save-baseline'], JSON.stringify(baselineData, null, 2));
180
+ console.log(`Baseline saved to: ${opts['save-baseline']}`);
181
+ }
package/src/executor.ts CHANGED
@@ -2,7 +2,7 @@ import { Worker } from 'node:worker_threads';
2
2
  import { once } from 'node:events';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { createReport, computeStats, Report } from './reporter.js';
5
- import { cmp, assertNoClosure } from './utils.js';
5
+ import { cmp, assertNoClosure, normalizeFunction } from './utils.js';
6
6
  import {
7
7
  type ExecutorRunOptions,
8
8
  type ReportOptions,
@@ -43,6 +43,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
43
43
  const resolvedBenchmarkUrl = typeof benchmarkUrl === 'string' ? benchmarkUrl : pathToFileURL(process.cwd()).href;
44
44
 
45
45
  const pending: { task: ExecutorRunOptions<TContext, TInput>; resolve: (v: unknown) => void; reject: (e: unknown) => void }[] = [];
46
+ const activeWorkers = new Set<Worker>();
46
47
  let running = 0;
47
48
 
48
49
  const schedule = async (task: ExecutorRunOptions<TContext, TInput>) => {
@@ -68,11 +69,11 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
68
69
  };
69
70
 
70
71
  const runTask = async ({ id, setup, teardown, pre, run, post, data }: ExecutorRunOptions<TContext, TInput>) => {
71
- const setupCode = setup?.toString();
72
- const teardownCode = teardown?.toString();
73
- const preCode = pre?.toString();
74
- const runCode = run.toString();
75
- const postCode = post?.toString();
72
+ const setupCode = setup ? normalizeFunction(setup.toString()) : undefined;
73
+ const teardownCode = teardown ? normalizeFunction(teardown.toString()) : undefined;
74
+ const preCode = pre ? normalizeFunction(pre.toString()) : undefined;
75
+ const runCode = normalizeFunction(run.toString());
76
+ const postCode = post ? normalizeFunction(post.toString()) : undefined;
76
77
 
77
78
  if (setupCode) assertNoClosure(setupCode, 'setup');
78
79
  if (teardownCode) assertNoClosure(teardownCode, 'teardown');
@@ -106,6 +107,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
106
107
  const worker = new Worker(workerFile, {
107
108
  workerData,
108
109
  });
110
+ activeWorkers.add(worker);
109
111
 
110
112
  const control = new Int32Array(controlSAB);
111
113
  let progressIntervalId: ReturnType<typeof setInterval> | undefined;
@@ -132,6 +134,7 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
132
134
  if (progressIntervalId) clearInterval(progressIntervalId);
133
135
  workerError = err instanceof Error ? err.message : String(err);
134
136
  }
137
+ activeWorkers.delete(worker);
135
138
 
136
139
  const count = control[Control.INDEX];
137
140
  const heapUsedKB = control[Control.HEAP_USED];
@@ -161,5 +164,13 @@ export const createExecutor = <TContext, TInput, R extends ReportTypeList>(optio
161
164
  return Object.fromEntries(report);
162
165
  };
163
166
 
164
- return { pushAsync, kill() {} };
167
+ return {
168
+ pushAsync,
169
+ kill() {
170
+ for (const w of activeWorkers) w.terminate();
171
+ activeWorkers.clear();
172
+ for (const p of pending) p.reject(new Error('Executor killed'));
173
+ pending.length = 0;
174
+ },
175
+ };
165
176
  };
package/src/runner.ts CHANGED
@@ -592,7 +592,6 @@ export const benchmark = async <TContext, TInput>({
592
592
  const durationNumber = Number(sampleDuration);
593
593
  if (!disableFiltering) {
594
594
  const { median, iqr } = medianAndIqr(outlierWindow);
595
- pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
596
595
  const maxAllowed = median + OUTLIER_IQR_MULTIPLIER * iqr || Number.POSITIVE_INFINITY;
597
596
  if (outlierWindow.length >= 8 && durationNumber > maxAllowed && durationNumber - median > OUTLIER_ABS_THRESHOLD) {
598
597
  skipped++;
@@ -604,9 +603,8 @@ export const benchmark = async <TContext, TInput>({
604
603
  skipped++;
605
604
  continue;
606
605
  }
607
- } else {
608
- pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
609
606
  }
607
+ pushWindow(outlierWindow, durationNumber, OUTLIER_WINDOW);
610
608
 
611
609
  durations[i++] = sampleDuration;
612
610
  const deltaS = sampleDuration * WELFORD_SCALE - meanS;
package/src/utils.ts CHANGED
@@ -45,11 +45,14 @@ export const max = (a: bigint, b: bigint) => {
45
45
 
46
46
  export function div(a: bigint, b: bigint, decimals: number = 2): string {
47
47
  if (b === 0n) throw new RangeError('Division by zero');
48
+ const neg = a < 0n !== b < 0n;
49
+ const absA = a < 0n ? -a : a;
50
+ const absB = b < 0n ? -b : b;
48
51
  const scale = 10n ** BigInt(decimals);
49
- const scaled = (a * scale) / b;
52
+ const scaled = (absA * scale) / absB;
50
53
  const intPart = scaled / scale;
51
54
  const fracPart = scaled % scale;
52
- return `${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
55
+ return `${neg ? '-' : ''}${intPart}.${fracPart.toString().padStart(decimals, '0')}`;
53
56
  }
54
57
 
55
58
  export function divs(a: bigint, b: bigint, scale: bigint): bigint {
@@ -60,19 +63,68 @@ export function divs(a: bigint, b: bigint, scale: bigint): bigint {
60
63
  const KNOWN_GLOBALS = new Set(Object.getOwnPropertyNames(globalThis));
61
64
  KNOWN_GLOBALS.add('arguments');
62
65
 
63
- function collectUnresolved(node: unknown, result: Set<string>) {
66
+ let _unresolvedCtxt: number | undefined;
67
+
68
+ function findIdentifierCtxt(node: unknown, name: string): number | undefined {
69
+ if (!node || typeof node !== 'object') return undefined;
70
+ if (Array.isArray(node)) {
71
+ for (const item of node) {
72
+ const r = findIdentifierCtxt(item, name);
73
+ if (r !== undefined) return r;
74
+ }
75
+ return undefined;
76
+ }
77
+ const obj = node as Record<string, unknown>;
78
+ if (obj.type === 'Identifier' && obj.value === name && typeof obj.ctxt === 'number') {
79
+ return obj.ctxt;
80
+ }
81
+ for (const key of Object.keys(obj)) {
82
+ if (key === 'span') continue;
83
+ const r = findIdentifierCtxt(obj[key], name);
84
+ if (r !== undefined) return r;
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ function probeUnresolvedCtxt(): number {
90
+ if (_unresolvedCtxt !== undefined) return _unresolvedCtxt;
91
+ try {
92
+ const ast = parseSync('var _ = () => __PROBE__', { syntax: 'ecmascript', target: 'esnext' });
93
+ _unresolvedCtxt = findIdentifierCtxt(ast, '__PROBE__') ?? 1;
94
+ } catch {
95
+ _unresolvedCtxt = 1;
96
+ }
97
+ return _unresolvedCtxt;
98
+ }
99
+
100
+ function collectUnresolved(node: unknown, ctxt: number, result: Set<string>) {
64
101
  if (!node || typeof node !== 'object') return;
65
102
  if (Array.isArray(node)) {
66
- for (const item of node) collectUnresolved(item, result);
103
+ for (const item of node) collectUnresolved(item, ctxt, result);
67
104
  return;
68
105
  }
69
106
  const obj = node as Record<string, unknown>;
70
- if (obj.type === 'Identifier' && obj.ctxt === 1 && typeof obj.value === 'string') {
107
+ if (obj.type === 'Identifier' && obj.ctxt === ctxt && typeof obj.value === 'string') {
71
108
  result.add(obj.value);
72
109
  }
73
110
  for (const key of Object.keys(obj)) {
74
111
  if (key === 'span') continue;
75
- collectUnresolved(obj[key], result);
112
+ collectUnresolved(obj[key], ctxt, result);
113
+ }
114
+ }
115
+
116
+ export function normalizeFunction(code: string): string {
117
+ try {
118
+ parseSync(`var __fn = ${code}`, { syntax: 'ecmascript', target: 'esnext' });
119
+ return code;
120
+ } catch {
121
+ const normalized = code.startsWith('async ') ? `async function ${code.slice(6)}` : `function ${code}`;
122
+ try {
123
+ parseSync(`var __fn = ${normalized}`, { syntax: 'ecmascript', target: 'esnext' });
124
+ return normalized;
125
+ } catch {
126
+ return code;
127
+ }
76
128
  }
77
129
  }
78
130
 
@@ -83,8 +135,9 @@ export function assertNoClosure(code: string, name: string): void {
83
135
  } catch {
84
136
  return;
85
137
  }
138
+ const unresolvedCtxt = probeUnresolvedCtxt();
86
139
  const unresolved = new Set<string>();
87
- collectUnresolved(ast, unresolved);
140
+ collectUnresolved(ast, unresolvedCtxt, unresolved);
88
141
  for (const g of KNOWN_GLOBALS) unresolved.delete(g);
89
142
  if (unresolved.size === 0) return;
90
143