394 lines
13 KiB
JavaScript
Executable File
394 lines
13 KiB
JavaScript
Executable File
|
|
import * as fs from 'fs/promises';
|
|
import * as inspector from 'inspector';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import * as process from 'process';
|
|
import * as url from 'url';
|
|
import * as worker_threads from 'worker_threads';
|
|
|
|
const clearLine = () => {
|
|
if (process.stdout.clearLine) {
|
|
process.stdout.clearLine();
|
|
}
|
|
};
|
|
|
|
const write = (message) => {
|
|
if (process.stdout.write) {
|
|
process.stdout.write(message);
|
|
}
|
|
};
|
|
|
|
const access = async (path) => {
|
|
try {
|
|
await fs.access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
const error = (e) => {
|
|
/* eslint-disable no-console */
|
|
console.error(`${e.name}: ${e.message}`);
|
|
if (e.stack) {
|
|
// console.error(e.stack);
|
|
}
|
|
if (e.cause) {
|
|
console.error(` ${e.cause.name}: ${e.cause.message}`);
|
|
}
|
|
};
|
|
|
|
const exit = (e) => {
|
|
error(e);
|
|
/* eslint-enable no-console */
|
|
process.exit(1);
|
|
};
|
|
|
|
const dirname = (...args) => {
|
|
const file = url.fileURLToPath(import.meta.url);
|
|
const dir = path.dirname(file);
|
|
return path.join(dir, ...args);
|
|
};
|
|
|
|
const configuration = async () => {
|
|
const file = dirname('models.json');
|
|
const content = await fs.readFile(file, 'utf-8');
|
|
return JSON.parse(content);
|
|
};
|
|
|
|
class Logger {
|
|
|
|
constructor(threads) {
|
|
this._threads = threads;
|
|
this._entries = new Map();
|
|
}
|
|
|
|
update(identifier, message) {
|
|
let value = null;
|
|
if (message) {
|
|
switch (message.name) {
|
|
case 'name':
|
|
delete this._cache;
|
|
clearLine();
|
|
write(`${message.target}\n`);
|
|
value = '';
|
|
break;
|
|
case 'download':
|
|
if (message.percent !== undefined) {
|
|
value = `${(` ${Math.floor(100 * message.percent)}`).slice(-3)}% `;
|
|
} else if (Number.isInteger(message.position)) {
|
|
value = ` ${message.position}${this._threads === 1 ? ' bytes' : ''} `;
|
|
} else {
|
|
value = ' \u2714 ';
|
|
}
|
|
break;
|
|
case 'decompress':
|
|
value = this._threads === 1 ? 'decompress' : ' ^ ';
|
|
break;
|
|
case 'write':
|
|
value = this._threads === 1 ? 'write' : ' * ';
|
|
break;
|
|
default:
|
|
throw new Error(`Unsupported status message '${message.name}'.`);
|
|
}
|
|
}
|
|
if (!this._entries.has(identifier) || this._entries.get(identifier) !== value) {
|
|
this._entries.set(identifier, value);
|
|
this._flush();
|
|
}
|
|
}
|
|
|
|
delete(identifier) {
|
|
this._entries.delete(identifier);
|
|
this._flush();
|
|
}
|
|
|
|
flush() {
|
|
delete this._cache;
|
|
this._flush();
|
|
}
|
|
|
|
_flush() {
|
|
const values = Array.from(this._entries.values());
|
|
const text = values.some((s) => s) ? ` ${values.map((s) => s || ' ').join('-')}\r` : '';
|
|
if (this._cache !== text) {
|
|
this._cache = text;
|
|
clearLine();
|
|
write(text);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Queue extends Array {
|
|
|
|
constructor(targets, patterns) {
|
|
for (const target of targets) {
|
|
target.targets = target.target.split(',');
|
|
target.name = target.type ? `third_party/test/${target.type}/${target.targets[0]}` : target.targets[0];
|
|
target.tags = target.tags ? target.tags.split(',') : [];
|
|
}
|
|
if (patterns.length > 0) {
|
|
const tags = new Set();
|
|
patterns = patterns.filter((pattern) => {
|
|
if (pattern.startsWith('tag:')) {
|
|
tags.add(pattern.substring(4));
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
patterns = patterns.map((pattern) => {
|
|
const wildcard = pattern.indexOf('*') !== -1;
|
|
return new RegExp(`^${wildcard ? `${pattern.replace(/\*/g, '.*')}$` : pattern}`);
|
|
});
|
|
targets = targets.filter((target) => {
|
|
for (const file of target.targets) {
|
|
const value = target.type ? `${target.type}/${file}` : file;
|
|
if (patterns.some((pattern) => pattern.test(value))) {
|
|
return true;
|
|
}
|
|
if (target.tags.some((tag) => tags.has(tag))) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
super(...targets.reverse());
|
|
}
|
|
}
|
|
|
|
class Table {
|
|
|
|
constructor(schema) {
|
|
this.schema = schema;
|
|
const line = `${Array.from(this.schema).join(',')}\n`;
|
|
this.content = [line];
|
|
this.entries = [];
|
|
}
|
|
|
|
async add(row) {
|
|
this.entries.push(row);
|
|
row = new Map(row);
|
|
const line = `${Array.from(this.schema).map((key) => {
|
|
const value = row.has(key) ? row.get(key) : '';
|
|
row.delete(key);
|
|
return value;
|
|
}).join(',')}\n`;
|
|
if (row.size > 0) {
|
|
throw new Error();
|
|
}
|
|
this.content.push(line);
|
|
if (this.file) {
|
|
await fs.appendFile(this.file, line);
|
|
}
|
|
}
|
|
|
|
async log(file) {
|
|
if (file) {
|
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
await fs.writeFile(file, this.content.join(''));
|
|
this.file = file;
|
|
}
|
|
}
|
|
|
|
summarize(name) {
|
|
const entries = this.entries.filter((entry) => entry.has(name));
|
|
return entries.map((entry) => entry.get(name)).reduce((a, c) => a + c, 0);
|
|
}
|
|
}
|
|
|
|
class Worker {
|
|
|
|
constructor(identifier, queue, logger, measures) {
|
|
this._identifier = identifier;
|
|
this._queue = queue;
|
|
this._logger = logger;
|
|
this._measures = measures;
|
|
}
|
|
|
|
async start() {
|
|
this._events = {};
|
|
this._events.message = (message) => this._message(message);
|
|
this._events.error = (error) => this._error(error);
|
|
this._worker = new worker_threads.Worker('./test/worker.js');
|
|
for (let task = this._queue.pop(); task; task = this._queue.pop()) {
|
|
task.measures = this._measures ? new Map() : null;
|
|
this._logger.update(this._identifier, null);
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await new Promise((resolve) => {
|
|
this._resolve = resolve;
|
|
this._attach();
|
|
this._worker.postMessage(task);
|
|
});
|
|
}
|
|
this._logger.delete(this._identifier);
|
|
await this._worker.terminate();
|
|
}
|
|
|
|
_attach() {
|
|
this._worker.on('message', this._events.message);
|
|
this._worker.on('error', this._events.error);
|
|
}
|
|
|
|
_detach() {
|
|
this._worker.off('message', this._events.message);
|
|
this._worker.off('error', this._events.error);
|
|
}
|
|
|
|
async _message(message) {
|
|
switch (message.type) {
|
|
case 'status': {
|
|
this._logger.update(this._identifier, message);
|
|
break;
|
|
}
|
|
case 'error': {
|
|
write(`\n${message.target}\n`);
|
|
this._error(message.error);
|
|
break;
|
|
}
|
|
case 'complete': {
|
|
if (this._measures) {
|
|
await this._measures.add(message.measures);
|
|
}
|
|
this._detach();
|
|
this._resolve();
|
|
delete this._resolve;
|
|
break;
|
|
}
|
|
default: {
|
|
throw new Error(`Unsupported message type '${message.type}'.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
_error(error) {
|
|
this._detach();
|
|
delete this._resolve;
|
|
exit(error);
|
|
}
|
|
}
|
|
|
|
const main = async () => {
|
|
try {
|
|
const args = { inputs: [], measure: false, profile: false, continue: false, serial: false };
|
|
if (process.argv.length > 2) {
|
|
for (const arg of process.argv.slice(2)) {
|
|
switch (arg) {
|
|
case 'measure': args.measure = true; args.serial = true; break;
|
|
case 'profile': args.profile = true; args.serial = true; break;
|
|
case 'continue': args.continue = true; args.serial = true; break;
|
|
default: {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const exists = await access(arg);
|
|
if (exists || !/[*?[\]]/.test(arg)) {
|
|
args.inputs.push(arg);
|
|
} else {
|
|
const iterator = fs.glob(arg, { cwd: process.cwd() });
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const files = await Array.fromAsync(iterator);
|
|
args.inputs.push(...files);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const exists = await Promise.all(args.inputs.map((pattern) => access(pattern)));
|
|
const paths = exists.length > 0 && exists.every((value) => value);
|
|
const patterns = paths ? [] : args.inputs;
|
|
const targets = paths ? args.inputs.map((path) => ({ target: path, tags: 'quantization,validation' })) : await configuration();
|
|
const queue = new Queue(targets, patterns);
|
|
const threads = args.serial || inspector.url() ? 1 : undefined;
|
|
const logger = new Logger(threads);
|
|
let measures = null;
|
|
if (args.measure) {
|
|
measures = new Table(['name', 'download', 'load', 'validate', 'render']);
|
|
await measures.log(dirname('..', 'dist', 'test', 'measures.csv'));
|
|
}
|
|
let session = null;
|
|
if (args.profile) {
|
|
session = new inspector.Session();
|
|
session.connect();
|
|
await new Promise((resolve, reject) => {
|
|
session.post('Profiler.enable', (error) => error ? reject(error) : resolve());
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
session.post('Profiler.start', (error) => error ? reject(error) : resolve());
|
|
});
|
|
/* eslint-disable no-console */
|
|
console.profile();
|
|
/* eslint-enable no-console */
|
|
}
|
|
if (threads === 1) {
|
|
const worker = await import('./worker.js');
|
|
const total = queue.length;
|
|
let success = 0;
|
|
for (let item = queue.pop(); item; item = queue.pop()) {
|
|
const target = new worker.Target(item);
|
|
target.measures = measures ? new Map() : null;
|
|
target.serial = args.serial;
|
|
target.on('status', (sender, message) => logger.update('', message));
|
|
try {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await target.execute();
|
|
success += 1;
|
|
} catch (e) {
|
|
if (args.continue) {
|
|
error(e);
|
|
} else {
|
|
exit(e);
|
|
}
|
|
}
|
|
if (target.measures) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await measures.add(target.measures);
|
|
}
|
|
}
|
|
if (args.continue) {
|
|
write(` ${success} / ${total} = ${success * 100 / total}%\n`);
|
|
}
|
|
} else {
|
|
const threads = Math.max(1, Math.min(10, Math.round(0.7 * os.cpus().length), queue.length));
|
|
const identifiers = [...new Array(threads).keys()].map((value) => value.toString());
|
|
const workers = identifiers.map((identifier) => new Worker(identifier, queue, logger, measures));
|
|
const promises = workers.map((worker) => worker.start());
|
|
await Promise.all(promises);
|
|
}
|
|
if (args.measure) {
|
|
const values = {
|
|
download: measures.summarize('download'),
|
|
load: measures.summarize('load'),
|
|
validate: measures.summarize('validate'),
|
|
render: measures.summarize('render')
|
|
};
|
|
values.total = values.load + values.validate + values.render;
|
|
const pad1 = Math.max(...Object.keys(values).map((key) => key.length));
|
|
const pad2 = Math.max(...Object.values(values).map((value) => value.toFixed(2).indexOf('.')));
|
|
write('\n');
|
|
for (let [key, value] of Object.entries(values)) {
|
|
key = `${key}:`.padEnd(pad1 + 1);
|
|
value = `${value.toFixed(2)}`.padStart(pad2 + 3);
|
|
write(`${key} ${value}\n`);
|
|
}
|
|
write('\n');
|
|
}
|
|
if (args.profile) {
|
|
/* eslint-disable no-console */
|
|
console.profileEnd();
|
|
/* eslint-enable no-console */
|
|
const data = await new Promise((resolve, reject) => {
|
|
session.post('Profiler.stop', (error, data) => error ? reject(error) : resolve(data));
|
|
});
|
|
session.disconnect();
|
|
const file = dirname('..', 'dist', 'test', 'profile.cpuprofile');
|
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
await fs.writeFile(file, JSON.stringify(data.profile), 'utf-8');
|
|
}
|
|
} catch (error) {
|
|
exit(error);
|
|
}
|
|
};
|
|
|
|
await main();
|