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();