@parcel/workers 2.0.0-nightly.130 → 2.0.0-nightly.1303
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/index.d.ts +23 -0
- package/lib/Handle.js +14 -45
- package/lib/Worker.js +100 -27
- package/lib/WorkerFarm.js +260 -104
- package/lib/bus.js +10 -2
- package/lib/child.js +129 -62
- package/lib/childState.js +1 -1
- package/lib/cpuCount.js +28 -7
- package/lib/index.js +41 -10
- package/lib/process/ProcessChild.js +22 -11
- package/lib/process/ProcessWorker.js +30 -19
- package/lib/threads/ThreadsChild.js +32 -14
- package/lib/threads/ThreadsWorker.js +28 -16
- package/package.json +19 -8
- package/src/Handle.js +10 -39
- package/src/Worker.js +82 -15
- package/src/WorkerFarm.js +219 -54
- package/src/bus.js +1 -1
- package/src/child.js +83 -24
- package/src/cpuCount.js +9 -4
- package/src/index.js +8 -2
- package/src/process/ProcessChild.js +2 -1
- package/src/process/ProcessWorker.js +1 -1
- package/src/threads/ThreadsWorker.js +2 -2
- package/test/cpuCount.test.js +1 -1
- package/test/integration/workerfarm/console.js +1 -1
- package/test/integration/workerfarm/logging.js +1 -1
- package/test/integration/workerfarm/reverse-handle.js +2 -2
- package/test/workerfarm.js +5 -5
- package/lib/Profiler.js +0 -70
- package/lib/Trace.js +0 -126
- package/src/Profiler.js +0 -93
- package/src/Trace.js +0 -121
package/src/WorkerFarm.js
CHANGED
@@ -20,20 +20,20 @@ import {
|
|
20
20
|
restoreDeserializedObject,
|
21
21
|
serialize,
|
22
22
|
} from '@parcel/core';
|
23
|
-
import ThrowableDiagnostic, {anyToDiagnostic} from '@parcel/diagnostic';
|
23
|
+
import ThrowableDiagnostic, {anyToDiagnostic, md} from '@parcel/diagnostic';
|
24
24
|
import Worker, {type WorkerCall} from './Worker';
|
25
25
|
import cpuCount from './cpuCount';
|
26
26
|
import Handle from './Handle';
|
27
27
|
import {child} from './childState';
|
28
28
|
import {detectBackend} from './backend';
|
29
|
-
import
|
30
|
-
import Trace from './Trace';
|
29
|
+
import {SamplingProfiler, Trace} from '@parcel/profiler';
|
31
30
|
import fs from 'fs';
|
32
31
|
import logger from '@parcel/logger';
|
33
32
|
|
34
|
-
let profileId = 1;
|
35
33
|
let referenceId = 1;
|
36
34
|
|
35
|
+
export opaque type SharedReference = number;
|
36
|
+
|
37
37
|
export type FarmOptions = {|
|
38
38
|
maxConcurrentWorkers: number,
|
39
39
|
maxConcurrentCallsPerWorker: number,
|
@@ -42,7 +42,8 @@ export type FarmOptions = {|
|
|
42
42
|
warmWorkers: boolean,
|
43
43
|
workerPath?: FilePath,
|
44
44
|
backend: BackendType,
|
45
|
-
|
45
|
+
shouldPatchConsole?: boolean,
|
46
|
+
shouldTrace?: boolean,
|
46
47
|
|};
|
47
48
|
|
48
49
|
type WorkerModule = {|
|
@@ -52,8 +53,8 @@ type WorkerModule = {|
|
|
52
53
|
export type WorkerApi = {|
|
53
54
|
callMaster(CallRequest, ?boolean): Promise<mixed>,
|
54
55
|
createReverseHandle(fn: HandleFunction): Handle,
|
55
|
-
getSharedReference(ref:
|
56
|
-
resolveSharedReference(value: mixed): ?
|
56
|
+
getSharedReference(ref: SharedReference): mixed,
|
57
|
+
resolveSharedReference(value: mixed): ?SharedReference,
|
57
58
|
callChild?: (childId: number, request: HandleCallRequest) => Promise<mixed>,
|
58
59
|
|};
|
59
60
|
|
@@ -67,14 +68,16 @@ export default class WorkerFarm extends EventEmitter {
|
|
67
68
|
callQueue: Array<WorkerCall> = [];
|
68
69
|
ending: boolean = false;
|
69
70
|
localWorker: WorkerModule;
|
71
|
+
localWorkerInit: ?Promise<void>;
|
70
72
|
options: FarmOptions;
|
71
73
|
run: HandleFunction;
|
72
74
|
warmWorkers: number = 0;
|
73
75
|
workers: Map<number, Worker> = new Map();
|
74
76
|
handles: Map<number, Handle> = new Map();
|
75
|
-
sharedReferences: Map<
|
76
|
-
sharedReferencesByValue: Map<mixed,
|
77
|
-
|
77
|
+
sharedReferences: Map<SharedReference, mixed> = new Map();
|
78
|
+
sharedReferencesByValue: Map<mixed, SharedReference> = new Map();
|
79
|
+
serializedSharedReferences: Map<SharedReference, ?ArrayBuffer> = new Map();
|
80
|
+
profiler: ?SamplingProfiler;
|
78
81
|
|
79
82
|
constructor(farmOptions: $Shape<FarmOptions> = {}) {
|
80
83
|
super();
|
@@ -94,12 +97,39 @@ export default class WorkerFarm extends EventEmitter {
|
|
94
97
|
|
95
98
|
// $FlowFixMe this must be dynamic
|
96
99
|
this.localWorker = require(this.options.workerPath);
|
100
|
+
this.localWorkerInit =
|
101
|
+
this.localWorker.childInit != null ? this.localWorker.childInit() : null;
|
97
102
|
this.run = this.createHandle('run');
|
98
103
|
|
104
|
+
// Worker thread stdout is by default piped into the process stdout, if there are enough worker
|
105
|
+
// threads to exceed the default listener limit, then anything else piping into stdout will trigger
|
106
|
+
// the `MaxListenersExceededWarning`, so we should ensure the max listeners is at least equal to the
|
107
|
+
// number of workers + 1 for the main thread.
|
108
|
+
//
|
109
|
+
// Note this can't be fixed easily where other things pipe into stdout - even after starting > 10 worker
|
110
|
+
// threads `process.stdout.getMaxListeners()` will still return 10, however adding another pipe into `stdout`
|
111
|
+
// will give the warning with `<worker count + 1>` as the number of listeners.
|
112
|
+
process.stdout.setMaxListeners(
|
113
|
+
Math.max(
|
114
|
+
process.stdout.getMaxListeners(),
|
115
|
+
WorkerFarm.getNumWorkers() + 1,
|
116
|
+
),
|
117
|
+
);
|
118
|
+
|
99
119
|
this.startMaxWorkers();
|
100
120
|
}
|
101
121
|
|
102
|
-
workerApi
|
122
|
+
workerApi: {|
|
123
|
+
callChild: (childId: number, request: HandleCallRequest) => Promise<mixed>,
|
124
|
+
callMaster: (
|
125
|
+
request: CallRequest,
|
126
|
+
awaitResponse?: ?boolean,
|
127
|
+
) => Promise<mixed>,
|
128
|
+
createReverseHandle: (fn: HandleFunction) => Handle,
|
129
|
+
getSharedReference: (ref: SharedReference) => mixed,
|
130
|
+
resolveSharedReference: (value: mixed) => void | SharedReference,
|
131
|
+
runHandle: (handle: Handle, args: Array<any>) => Promise<mixed>,
|
132
|
+
|} = {
|
103
133
|
callMaster: async (
|
104
134
|
request: CallRequest,
|
105
135
|
awaitResponse: ?boolean = true,
|
@@ -122,7 +152,13 @@ export default class WorkerFarm extends EventEmitter {
|
|
122
152
|
retries: 0,
|
123
153
|
});
|
124
154
|
}),
|
125
|
-
|
155
|
+
runHandle: (handle: Handle, args: Array<any>): Promise<mixed> =>
|
156
|
+
this.workerApi.callChild(nullthrows(handle.childId), {
|
157
|
+
handle: handle.id,
|
158
|
+
args,
|
159
|
+
}),
|
160
|
+
getSharedReference: (ref: SharedReference) =>
|
161
|
+
this.sharedReferences.get(ref),
|
126
162
|
resolveSharedReference: (value: mixed) =>
|
127
163
|
this.sharedReferencesByValue.get(value),
|
128
164
|
};
|
@@ -155,30 +191,46 @@ export default class WorkerFarm extends EventEmitter {
|
|
155
191
|
);
|
156
192
|
}
|
157
193
|
|
158
|
-
createHandle(method: string): HandleFunction {
|
159
|
-
|
194
|
+
createHandle(method: string, useMainThread: boolean = false): HandleFunction {
|
195
|
+
if (!this.options.useLocalWorker) {
|
196
|
+
useMainThread = false;
|
197
|
+
}
|
198
|
+
|
199
|
+
return async (...args) => {
|
160
200
|
// Child process workers are slow to start (~600ms).
|
161
201
|
// While we're waiting, just run on the main thread.
|
162
202
|
// This significantly speeds up startup time.
|
163
|
-
if (this.shouldUseRemoteWorkers()) {
|
203
|
+
if (this.shouldUseRemoteWorkers() && !useMainThread) {
|
164
204
|
return this.addCall(method, [...args, false]);
|
165
205
|
} else {
|
166
206
|
if (this.options.warmWorkers && this.shouldStartRemoteWorkers()) {
|
167
207
|
this.warmupWorker(method, args);
|
168
208
|
}
|
169
209
|
|
170
|
-
let processedArgs
|
171
|
-
|
172
|
-
|
210
|
+
let processedArgs;
|
211
|
+
if (!useMainThread) {
|
212
|
+
processedArgs = restoreDeserializedObject(
|
213
|
+
prepareForSerialization([...args, false]),
|
214
|
+
);
|
215
|
+
} else {
|
216
|
+
processedArgs = args;
|
217
|
+
}
|
218
|
+
|
219
|
+
if (this.localWorkerInit != null) {
|
220
|
+
await this.localWorkerInit;
|
221
|
+
this.localWorkerInit = null;
|
222
|
+
}
|
173
223
|
return this.localWorker[method](this.workerApi, ...processedArgs);
|
174
224
|
}
|
175
225
|
};
|
176
226
|
}
|
177
227
|
|
178
|
-
onError(error: ErrorWithCode, worker: Worker) {
|
228
|
+
onError(error: ErrorWithCode, worker: Worker): void | Promise<void> {
|
179
229
|
// Handle ipc errors
|
180
230
|
if (error.code === 'ERR_IPC_CHANNEL_CLOSED') {
|
181
231
|
return this.stopWorker(worker);
|
232
|
+
} else {
|
233
|
+
logger.error(error, '@parcel/workers');
|
182
234
|
}
|
183
235
|
}
|
184
236
|
|
@@ -186,7 +238,9 @@ export default class WorkerFarm extends EventEmitter {
|
|
186
238
|
let worker = new Worker({
|
187
239
|
forcedKillTime: this.options.forcedKillTime,
|
188
240
|
backend: this.options.backend,
|
189
|
-
|
241
|
+
shouldPatchConsole: this.options.shouldPatchConsole,
|
242
|
+
shouldTrace: this.options.shouldTrace,
|
243
|
+
sharedReferences: this.sharedReferences,
|
190
244
|
});
|
191
245
|
|
192
246
|
worker.fork(nullthrows(this.options.workerPath));
|
@@ -231,7 +285,11 @@ export default class WorkerFarm extends EventEmitter {
|
|
231
285
|
this.startChild();
|
232
286
|
}
|
233
287
|
|
234
|
-
|
288
|
+
let workers = [...this.workers.values()].sort(
|
289
|
+
(a, b) => a.calls.size - b.calls.size,
|
290
|
+
);
|
291
|
+
|
292
|
+
for (let worker of workers) {
|
235
293
|
if (!this.callQueue.length) {
|
236
294
|
break;
|
237
295
|
}
|
@@ -241,11 +299,24 @@ export default class WorkerFarm extends EventEmitter {
|
|
241
299
|
}
|
242
300
|
|
243
301
|
if (worker.calls.size < this.options.maxConcurrentCallsPerWorker) {
|
244
|
-
|
302
|
+
this.callWorker(worker, this.callQueue.shift());
|
245
303
|
}
|
246
304
|
}
|
247
305
|
}
|
248
306
|
|
307
|
+
async callWorker(worker: Worker, call: WorkerCall): Promise<void> {
|
308
|
+
for (let ref of this.sharedReferences.keys()) {
|
309
|
+
if (!worker.sentSharedReferences.has(ref)) {
|
310
|
+
await worker.sendSharedReference(
|
311
|
+
ref,
|
312
|
+
this.getSerializedSharedReference(ref),
|
313
|
+
);
|
314
|
+
}
|
315
|
+
}
|
316
|
+
|
317
|
+
worker.call(call);
|
318
|
+
}
|
319
|
+
|
249
320
|
async processRequest(
|
250
321
|
data: {|
|
251
322
|
location: FilePath,
|
@@ -255,7 +326,7 @@ export default class WorkerFarm extends EventEmitter {
|
|
255
326
|
let {method, args, location, awaitResponse, idx, handle: handleId} = data;
|
256
327
|
let mod;
|
257
328
|
if (handleId != null) {
|
258
|
-
mod = nullthrows(this.handles.get(handleId))
|
329
|
+
mod = nullthrows(this.handles.get(handleId)?.fn);
|
259
330
|
} else if (location) {
|
260
331
|
// $FlowFixMe this must be dynamic
|
261
332
|
mod = require(location);
|
@@ -286,7 +357,6 @@ export default class WorkerFarm extends EventEmitter {
|
|
286
357
|
}
|
287
358
|
} else {
|
288
359
|
// ESModule default interop
|
289
|
-
// $FlowFixMe
|
290
360
|
if (mod.__esModule && !mod[method] && mod.default) {
|
291
361
|
mod = mod.default;
|
292
362
|
}
|
@@ -331,6 +401,10 @@ export default class WorkerFarm extends EventEmitter {
|
|
331
401
|
async end(): Promise<void> {
|
332
402
|
this.ending = true;
|
333
403
|
|
404
|
+
await Promise.all(
|
405
|
+
Array.from(this.workers.values()).map(worker => this.stopWorker(worker)),
|
406
|
+
);
|
407
|
+
|
334
408
|
for (let handle of this.handles.values()) {
|
335
409
|
handle.dispose();
|
336
410
|
}
|
@@ -338,9 +412,6 @@ export default class WorkerFarm extends EventEmitter {
|
|
338
412
|
this.sharedReferences = new Map();
|
339
413
|
this.sharedReferencesByValue = new Map();
|
340
414
|
|
341
|
-
await Promise.all(
|
342
|
-
Array.from(this.workers.values()).map(worker => this.stopWorker(worker)),
|
343
|
-
);
|
344
415
|
this.ending = false;
|
345
416
|
}
|
346
417
|
|
@@ -362,40 +433,37 @@ export default class WorkerFarm extends EventEmitter {
|
|
362
433
|
);
|
363
434
|
}
|
364
435
|
|
365
|
-
createReverseHandle(fn: HandleFunction) {
|
366
|
-
let handle = new Handle({fn
|
436
|
+
createReverseHandle(fn: HandleFunction): Handle {
|
437
|
+
let handle = new Handle({fn});
|
367
438
|
this.handles.set(handle.id, handle);
|
368
439
|
return handle;
|
369
440
|
}
|
370
441
|
|
371
|
-
|
442
|
+
createSharedReference(
|
443
|
+
value: mixed,
|
444
|
+
isCacheable: boolean = true,
|
445
|
+
): {|ref: SharedReference, dispose(): Promise<mixed>|} {
|
372
446
|
let ref = referenceId++;
|
373
447
|
this.sharedReferences.set(ref, value);
|
374
448
|
this.sharedReferencesByValue.set(value, ref);
|
375
|
-
|
376
|
-
|
377
|
-
promises.push(
|
378
|
-
new Promise((resolve, reject) => {
|
379
|
-
worker.call({
|
380
|
-
method: 'createSharedReference',
|
381
|
-
args: [ref, value],
|
382
|
-
resolve,
|
383
|
-
reject,
|
384
|
-
retries: 0,
|
385
|
-
});
|
386
|
-
}),
|
387
|
-
);
|
449
|
+
if (!isCacheable) {
|
450
|
+
this.serializedSharedReferences.set(ref, null);
|
388
451
|
}
|
389
452
|
|
390
|
-
await Promise.all(promises);
|
391
|
-
|
392
453
|
return {
|
393
454
|
ref,
|
394
455
|
dispose: () => {
|
395
456
|
this.sharedReferences.delete(ref);
|
396
457
|
this.sharedReferencesByValue.delete(value);
|
458
|
+
this.serializedSharedReferences.delete(ref);
|
459
|
+
|
397
460
|
let promises = [];
|
398
461
|
for (let worker of this.workers.values()) {
|
462
|
+
if (!worker.sentSharedReferences.has(ref)) {
|
463
|
+
continue;
|
464
|
+
}
|
465
|
+
|
466
|
+
worker.sentSharedReferences.delete(ref);
|
399
467
|
promises.push(
|
400
468
|
new Promise((resolve, reject) => {
|
401
469
|
worker.call({
|
@@ -403,6 +471,7 @@ export default class WorkerFarm extends EventEmitter {
|
|
403
471
|
args: [ref],
|
404
472
|
resolve,
|
405
473
|
reject,
|
474
|
+
skipReadyCheck: true,
|
406
475
|
retries: 0,
|
407
476
|
});
|
408
477
|
}),
|
@@ -413,6 +482,24 @@ export default class WorkerFarm extends EventEmitter {
|
|
413
482
|
};
|
414
483
|
}
|
415
484
|
|
485
|
+
getSerializedSharedReference(ref: SharedReference): ArrayBuffer {
|
486
|
+
let cached = this.serializedSharedReferences.get(ref);
|
487
|
+
if (cached) {
|
488
|
+
return cached;
|
489
|
+
}
|
490
|
+
|
491
|
+
let value = this.sharedReferences.get(ref);
|
492
|
+
let buf = serialize(value).buffer;
|
493
|
+
|
494
|
+
// If the reference was created with the isCacheable option set to false,
|
495
|
+
// serializedSharedReferences will contain `null` as the value.
|
496
|
+
if (cached !== null) {
|
497
|
+
this.serializedSharedReferences.set(ref, buf);
|
498
|
+
}
|
499
|
+
|
500
|
+
return buf;
|
501
|
+
}
|
502
|
+
|
416
503
|
async startProfile() {
|
417
504
|
let promises = [];
|
418
505
|
for (let worker of this.workers.values()) {
|
@@ -424,12 +511,13 @@ export default class WorkerFarm extends EventEmitter {
|
|
424
511
|
resolve,
|
425
512
|
reject,
|
426
513
|
retries: 0,
|
514
|
+
skipReadyCheck: true,
|
427
515
|
});
|
428
516
|
}),
|
429
517
|
);
|
430
518
|
}
|
431
519
|
|
432
|
-
this.profiler = new
|
520
|
+
this.profiler = new SamplingProfiler();
|
433
521
|
|
434
522
|
promises.push(this.profiler.startProfiling());
|
435
523
|
await Promise.all(promises);
|
@@ -453,6 +541,7 @@ export default class WorkerFarm extends EventEmitter {
|
|
453
541
|
resolve,
|
454
542
|
reject,
|
455
543
|
retries: 0,
|
544
|
+
skipReadyCheck: true,
|
456
545
|
});
|
457
546
|
}),
|
458
547
|
);
|
@@ -460,7 +549,7 @@ export default class WorkerFarm extends EventEmitter {
|
|
460
549
|
|
461
550
|
var profiles = await Promise.all(promises);
|
462
551
|
let trace = new Trace();
|
463
|
-
let filename = `profile-${
|
552
|
+
let filename = `profile-${getTimeId()}.trace`;
|
464
553
|
let stream = trace.pipe(fs.createWriteStream(filename));
|
465
554
|
|
466
555
|
for (let profile of profiles) {
|
@@ -474,21 +563,84 @@ export default class WorkerFarm extends EventEmitter {
|
|
474
563
|
|
475
564
|
logger.info({
|
476
565
|
origin: '@parcel/workers',
|
477
|
-
message: `Wrote profile to ${filename}`,
|
566
|
+
message: md`Wrote profile to ${filename}`,
|
478
567
|
});
|
479
568
|
}
|
480
569
|
|
481
|
-
|
570
|
+
async callAllWorkers(method: string, args: Array<any>) {
|
571
|
+
let promises = [];
|
572
|
+
for (let worker of this.workers.values()) {
|
573
|
+
promises.push(
|
574
|
+
new Promise((resolve, reject) => {
|
575
|
+
worker.call({
|
576
|
+
method,
|
577
|
+
args,
|
578
|
+
resolve,
|
579
|
+
reject,
|
580
|
+
retries: 0,
|
581
|
+
});
|
582
|
+
}),
|
583
|
+
);
|
584
|
+
}
|
585
|
+
|
586
|
+
promises.push(this.localWorker[method](this.workerApi, ...args));
|
587
|
+
await Promise.all(promises);
|
588
|
+
}
|
589
|
+
|
590
|
+
async takeHeapSnapshot() {
|
591
|
+
let snapshotId = getTimeId();
|
592
|
+
|
593
|
+
try {
|
594
|
+
let snapshotPaths = await Promise.all(
|
595
|
+
[...this.workers.values()].map(
|
596
|
+
worker =>
|
597
|
+
new Promise((resolve, reject) => {
|
598
|
+
worker.call({
|
599
|
+
method: 'takeHeapSnapshot',
|
600
|
+
args: [snapshotId],
|
601
|
+
resolve,
|
602
|
+
reject,
|
603
|
+
retries: 0,
|
604
|
+
skipReadyCheck: true,
|
605
|
+
});
|
606
|
+
}),
|
607
|
+
),
|
608
|
+
);
|
609
|
+
|
610
|
+
logger.info({
|
611
|
+
origin: '@parcel/workers',
|
612
|
+
message: md`Wrote heap snapshots to the following paths:\n${snapshotPaths.join(
|
613
|
+
'\n',
|
614
|
+
)}`,
|
615
|
+
});
|
616
|
+
} catch {
|
617
|
+
logger.error({
|
618
|
+
origin: '@parcel/workers',
|
619
|
+
message: 'Unable to take heap snapshots. Note: requires Node 11.13.0+',
|
620
|
+
});
|
621
|
+
}
|
622
|
+
}
|
623
|
+
|
624
|
+
static getNumWorkers(): number {
|
482
625
|
return process.env.PARCEL_WORKERS
|
483
626
|
? parseInt(process.env.PARCEL_WORKERS, 10)
|
484
|
-
: cpuCount();
|
627
|
+
: Math.ceil(cpuCount() / 2);
|
485
628
|
}
|
486
629
|
|
487
|
-
static isWorker() {
|
630
|
+
static isWorker(): boolean {
|
488
631
|
return !!child;
|
489
632
|
}
|
490
633
|
|
491
|
-
static getWorkerApi() {
|
634
|
+
static getWorkerApi(): {|
|
635
|
+
callMaster: (
|
636
|
+
request: CallRequest,
|
637
|
+
awaitResponse?: ?boolean,
|
638
|
+
) => Promise<mixed>,
|
639
|
+
createReverseHandle: (fn: (...args: Array<any>) => mixed) => Handle,
|
640
|
+
getSharedReference: (ref: SharedReference) => mixed,
|
641
|
+
resolveSharedReference: (value: mixed) => void | SharedReference,
|
642
|
+
runHandle: (handle: Handle, args: Array<any>) => Promise<mixed>,
|
643
|
+
|} {
|
492
644
|
invariant(
|
493
645
|
child != null,
|
494
646
|
'WorkerFarm.getWorkerApi can only be called within workers',
|
@@ -496,7 +648,20 @@ export default class WorkerFarm extends EventEmitter {
|
|
496
648
|
return child.workerApi;
|
497
649
|
}
|
498
650
|
|
499
|
-
static getConcurrentCallsPerWorker() {
|
500
|
-
return parseInt(process.env.PARCEL_MAX_CONCURRENT_CALLS, 10) ||
|
651
|
+
static getConcurrentCallsPerWorker(): number {
|
652
|
+
return parseInt(process.env.PARCEL_MAX_CONCURRENT_CALLS, 10) || 30;
|
501
653
|
}
|
502
654
|
}
|
655
|
+
|
656
|
+
function getTimeId() {
|
657
|
+
let now = new Date();
|
658
|
+
return (
|
659
|
+
String(now.getFullYear()) +
|
660
|
+
String(now.getMonth() + 1).padStart(2, '0') +
|
661
|
+
String(now.getDate()).padStart(2, '0') +
|
662
|
+
'-' +
|
663
|
+
String(now.getHours()).padStart(2, '0') +
|
664
|
+
String(now.getMinutes()).padStart(2, '0') +
|
665
|
+
String(now.getSeconds()).padStart(2, '0')
|
666
|
+
);
|
667
|
+
}
|
package/src/bus.js
CHANGED