models.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import * as fs from 'fs/promises';
  2. import * as inspector from 'inspector';
  3. import * as os from 'os';
  4. import * as path from 'path';
  5. import * as process from 'process';
  6. import * as url from 'url';
  7. import * as worker_threads from 'worker_threads';
  8. const clearLine = () => {
  9. if (process.stdout.clearLine) {
  10. process.stdout.clearLine();
  11. }
  12. };
  13. const write = (message) => {
  14. if (process.stdout.write) {
  15. process.stdout.write(message);
  16. }
  17. };
  18. const access = async (path) => {
  19. try {
  20. await fs.access(path);
  21. return true;
  22. } catch (error) {
  23. return false;
  24. }
  25. };
  26. const exit = (error) => {
  27. /* eslint-disable no-console */
  28. console.error(`${error.name}: ${error.message}`);
  29. if (error.cause) {
  30. console.error(` ${error.cause.name}: ${error.cause.message}`);
  31. }
  32. /* eslint-enable no-console */
  33. process.exit(1);
  34. };
  35. const dirname = (...args) => {
  36. const file = url.fileURLToPath(import.meta.url);
  37. const dir = path.dirname(file);
  38. return path.join(dir, ...args);
  39. };
  40. const configuration = async () => {
  41. const file = dirname('models.json');
  42. const content = await fs.readFile(file, 'utf-8');
  43. return JSON.parse(content);
  44. };
  45. class Logger {
  46. constructor(threads) {
  47. this._threads = threads;
  48. this._entries = new Map();
  49. }
  50. update(identifier, message) {
  51. let value = null;
  52. if (message) {
  53. switch (message.name) {
  54. case 'name':
  55. delete this._cache;
  56. clearLine();
  57. write(`${message.target}\n`);
  58. value = '';
  59. break;
  60. case 'download':
  61. value = message.percent !== undefined ?
  62. `${(` ${Math.floor(100 * message.percent)}`).slice(-3)}% ` :
  63. ` ${message.position}${this._threads === 1 ? ' bytes' : ''} `;
  64. break;
  65. case 'decompress':
  66. value = this._threads === 1 ? 'decompress' : ' ^ ';
  67. break;
  68. case 'write':
  69. value = this._threads === 1 ? 'write' : ' * ';
  70. break;
  71. default:
  72. throw new Error(`Unsupported status message '${message.name}'.`);
  73. }
  74. }
  75. if (!this._entries.has(identifier) || this._entries.get(identifier) !== value) {
  76. this._entries.set(identifier, value);
  77. this._flush();
  78. }
  79. }
  80. delete(identifier) {
  81. this._entries.delete(identifier);
  82. this._flush();
  83. }
  84. flush() {
  85. delete this._cache;
  86. this._flush();
  87. }
  88. _flush() {
  89. const values = Array.from(this._entries.values());
  90. const text = values.some((s) => s) ? ` ${values.map((s) => s || ' ').join('-')}\r` : '';
  91. if (this._cache !== text) {
  92. this._cache = text;
  93. clearLine();
  94. write(text);
  95. }
  96. }
  97. }
  98. class Queue extends Array {
  99. constructor(targets, patterns) {
  100. for (const target of targets) {
  101. target.targets = target.target.split(',');
  102. target.name = target.type ? `${target.type}/${target.targets[0]}` : target.targets[0];
  103. target.tags = target.tags? target.tags.split(',') : [];
  104. }
  105. if (patterns.length > 0) {
  106. const tags = new Set();
  107. patterns = patterns.filter((pattern) => {
  108. if (pattern.startsWith('tag:')) {
  109. tags.add(pattern.substring(4));
  110. return false;
  111. }
  112. return true;
  113. });
  114. patterns = patterns.map((pattern) => {
  115. const wildcard = pattern.indexOf('*') !== -1;
  116. return new RegExp(`^${wildcard ? `${pattern.replace(/\*/g, '.*')}$` : pattern}`);
  117. });
  118. targets = targets.filter((target) => {
  119. for (const file of target.targets) {
  120. const value = target.type ? `${target.type}/${file}` : file;
  121. if (patterns.some((pattern) => pattern.test(value))) {
  122. return true;
  123. }
  124. if (target.tags.some((tag) => tags.has(tag))) {
  125. return true;
  126. }
  127. }
  128. return false;
  129. });
  130. }
  131. super(...targets.reverse());
  132. }
  133. }
  134. class Table {
  135. constructor(schema) {
  136. this.schema = schema;
  137. const line = `${Array.from(this.schema).join(',')}\n`;
  138. this.content = [ line ];
  139. }
  140. async add(row) {
  141. row = new Map(row);
  142. const line = `${Array.from(this.schema).map((key) => {
  143. const value = row.has(key) ? row.get(key) : '';
  144. row.delete(key);
  145. return value;
  146. }).join(',')}\n`;
  147. if (row.size > 0) {
  148. throw new Error();
  149. }
  150. this.content.push(line);
  151. if (this.file) {
  152. await fs.appendFile(this.file, line);
  153. }
  154. }
  155. async log(file) {
  156. if (file) {
  157. await fs.mkdir(path.dirname(file), { recursive: true });
  158. await fs.writeFile(file, this.content.join(''));
  159. this.file = file;
  160. }
  161. }
  162. }
  163. class Worker {
  164. constructor(identifier, queue, logger, measures) {
  165. this._identifier = identifier;
  166. this._queue = queue;
  167. this._logger = logger;
  168. this._measures = measures;
  169. }
  170. async start() {
  171. this._events = {};
  172. this._events.message = (message) => this._message(message);
  173. this._events.error = (error) => this._error(error);
  174. this._worker = new worker_threads.Worker('./test/worker.js');
  175. for (let task = this._queue.pop(); task; task = this._queue.pop()) {
  176. this._logger.update(this._identifier, null);
  177. /* eslint-disable no-await-in-loop */
  178. await new Promise((resolve) => {
  179. this._resolve = resolve;
  180. this._attach();
  181. this._worker.postMessage(task);
  182. });
  183. /* eslint-enable no-await-in-loop */
  184. }
  185. this._logger.delete(this._identifier);
  186. await this._worker.terminate();
  187. }
  188. _attach() {
  189. this._worker.on('message', this._events.message);
  190. this._worker.on('error', this._events.error);
  191. }
  192. _detach() {
  193. this._worker.off('message', this._events.message);
  194. this._worker.off('error', this._events.error);
  195. }
  196. async _message(message) {
  197. switch (message.type) {
  198. case 'status': {
  199. this._logger.update(this._identifier, message);
  200. break;
  201. }
  202. case 'error': {
  203. write(`\n${message.target}\n`);
  204. this._error(message.error);
  205. break;
  206. }
  207. case 'complete': {
  208. await this._measures.add(message.measures);
  209. this._detach();
  210. this._resolve();
  211. delete this._resolve;
  212. break;
  213. }
  214. default: {
  215. throw new Error(`Unsupported message type '${message.type}'.`);
  216. }
  217. }
  218. }
  219. _error(error) {
  220. this._detach();
  221. delete this._resolve;
  222. exit(error);
  223. }
  224. }
  225. const main = async () => {
  226. try {
  227. const args = process.argv.length > 2 ? process.argv.slice(2) : [];
  228. const exists = await Promise.all(args.map((pattern) => access(pattern)));
  229. const paths = exists.length > 0 && exists.every((value) => value);
  230. const patterns = paths ? [] : args;
  231. const targets = paths ? args.map((path) => ({ target: path })) : await configuration();
  232. const queue = new Queue(targets, patterns);
  233. const threads = inspector.url() ? 1 : undefined;
  234. const logger = new Logger(threads);
  235. const measures = new Table([ 'name', 'download', 'load', 'validate', 'render' ]);
  236. // await measures.log(dirname('..', 'dist', 'test', 'measures.csv'));
  237. if (threads === 1) {
  238. const worker = await import('./worker.js');
  239. for (let item = queue.pop(); item; item = queue.pop()) {
  240. const target = new worker.Target(item);
  241. target.on('status', (_, message) => logger.update('', message));
  242. /* eslint-disable no-await-in-loop */
  243. await target.execute();
  244. await measures.add(target.measures);
  245. /* eslint-enable no-await-in-loop */
  246. }
  247. } else {
  248. const cores = Math.min(10, Math.round(0.7 * os.cpus().length), queue.length);
  249. const identifiers = [...new Array(cores).keys()].map((value) => value.toString());
  250. const workers = identifiers.map((identifier) => new Worker(identifier, queue, logger, measures));
  251. const promises = workers.map((worker) => worker.start());
  252. await Promise.all(promises);
  253. }
  254. } catch (error) {
  255. exit(error);
  256. }
  257. };
  258. main();