@woosh/meep-engine 2.49.6 → 2.49.7
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/package.json +1 -1
- package/src/core/cache/LoadingCache.d.ts +2 -0
- package/src/core/cache/LoadingCache.js +45 -18
- package/src/core/math/computeGreatestCommonDivisor.spec.js +9 -0
- package/src/core/math/statistics/computeStatisticalMean.spec.js +9 -0
- package/src/core/math/statistics/halton_sequence.spec.js +6 -1
- package/src/core/model/node-graph/node/Port.spec.js +10 -1
- package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.js +5 -8
- package/src/core/model/reactive/evaluation/MultiPredicateEvaluator.spec.js +41 -0
- package/src/core/model/reactive/trigger/BlackboardTrigger.js +5 -0
- package/src/core/model/reactive/trigger/BlackboardTrigger.spec.js +24 -0
- package/src/core/parser/simple/readBooleanToken.js +38 -0
- package/src/core/parser/simple/readHexToken.js +95 -0
- package/src/core/parser/simple/readHexToken.spec.js +21 -0
- package/src/core/parser/simple/readIdentifierToken.js +43 -0
- package/src/core/parser/simple/readIdentifierToken.spec.js +32 -0
- package/src/core/parser/simple/readLiteralToken.js +96 -0
- package/src/core/parser/simple/readNumberToken.js +73 -0
- package/src/core/parser/simple/readNumberToken.spec.js +17 -0
- package/src/core/parser/simple/readReferenceToken.js +48 -0
- package/src/core/parser/simple/readReferenceToken.spec.js +18 -0
- package/src/core/parser/simple/readStringToken.js +94 -0
- package/src/core/parser/simple/readStringToken.spec.js +57 -0
- package/src/core/parser/simple/{readUnsignedInteger.js → readUnsignedIntegerToken.js} +1 -1
- package/src/core/parser/simple/readUnsignedIntegerToken.spec.js +6 -0
- package/src/core/process/executor/ConcurrentExecutor.js +147 -234
- package/src/engine/intelligence/blackboard/Blackboard.d.ts +4 -0
- package/src/engine/intelligence/blackboard/Blackboard.js +11 -0
- package/src/view/tooltip/gml/parser/readReferenceValueToken.js +2 -2
- package/src/core/parser/simple/SimpleParser.js +0 -464
- package/src/core/parser/simple/SimpleParser.spec.js +0 -105
|
@@ -7,7 +7,6 @@ import { IllegalStateException } from "../../fsm/exceptions/IllegalStateExceptio
|
|
|
7
7
|
import { objectKeyByValue } from "../../model/object/objectKeyByValue.js";
|
|
8
8
|
import { TaskSignal } from "../task/TaskSignal.js";
|
|
9
9
|
import TaskState from "../task/TaskState.js";
|
|
10
|
-
import { clamp } from "../../math/clamp.js";
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
*
|
|
@@ -22,13 +21,20 @@ function isGroup(t) {
|
|
|
22
21
|
* @class
|
|
23
22
|
*/
|
|
24
23
|
class ConcurrentExecutor {
|
|
24
|
+
#cycle_count = 0;
|
|
25
|
+
/**
|
|
26
|
+
* Handle of the last scheduled `setTimeout`
|
|
27
|
+
* @type {number}
|
|
28
|
+
*/
|
|
29
|
+
#timeout_handle = -1;
|
|
30
|
+
|
|
25
31
|
/**
|
|
26
32
|
*
|
|
27
33
|
* @param {number} [quietTime] in milliseconds
|
|
28
34
|
* @param {number} [workTime] in milliseconds
|
|
29
35
|
* @constructor
|
|
30
36
|
*/
|
|
31
|
-
constructor(quietTime=1, workTime=15) {
|
|
37
|
+
constructor(quietTime = 1, workTime = 15) {
|
|
32
38
|
/**
|
|
33
39
|
*
|
|
34
40
|
* @type {number}
|
|
@@ -58,6 +64,8 @@ class ConcurrentExecutor {
|
|
|
58
64
|
completed: new Signal()
|
|
59
65
|
};
|
|
60
66
|
|
|
67
|
+
this.busy = false;
|
|
68
|
+
|
|
61
69
|
/**
|
|
62
70
|
*
|
|
63
71
|
* @type {number|SchedulingPolicy}
|
|
@@ -298,142 +306,188 @@ class ConcurrentExecutor {
|
|
|
298
306
|
}
|
|
299
307
|
|
|
300
308
|
/**
|
|
301
|
-
*
|
|
302
|
-
* @
|
|
309
|
+
*
|
|
310
|
+
* @return {Task|undefined}
|
|
303
311
|
*/
|
|
304
|
-
|
|
305
|
-
const
|
|
312
|
+
#pick_next_task() {
|
|
313
|
+
const ready = this.queueReady;
|
|
314
|
+
|
|
315
|
+
switch (this.policy) {
|
|
316
|
+
case ConcurrentExecutor.POLICY.ROUND_ROBIN:
|
|
317
|
+
return ready[this.#cycle_count % ready.length];
|
|
318
|
+
default:
|
|
319
|
+
console.warn('Unknown scheduling policy: ', this.policy, 'Defaulting to sequential');
|
|
320
|
+
// fallthrough
|
|
321
|
+
case ConcurrentExecutor.POLICY.SEQUENTIAL:
|
|
322
|
+
return ready[0];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
*
|
|
328
|
+
* @param {Task} task
|
|
329
|
+
*/
|
|
330
|
+
#complete_task(task) {
|
|
331
|
+
const readyTasks = this.queueReady;
|
|
332
|
+
|
|
333
|
+
const taskIndex = readyTasks.indexOf(task);
|
|
334
|
+
|
|
335
|
+
if (taskIndex !== -1) {
|
|
336
|
+
readyTasks.splice(taskIndex, 1);
|
|
337
|
+
} else {
|
|
338
|
+
console.error("Failed to remove ready task, not found in the ready queue", task, readyTasks.slice());
|
|
339
|
+
}
|
|
340
|
+
|
|
306
341
|
|
|
307
|
-
|
|
342
|
+
task.state.set(TaskState.SUCCEEDED);
|
|
343
|
+
task.on.completed.send1(this);
|
|
308
344
|
|
|
309
345
|
this.resolveTasks();
|
|
310
346
|
|
|
347
|
+
this.on.task_completed.send1(task);
|
|
348
|
+
// console.warn(`Task complete '${task.name}', cycles=${task.__executedCycleCount}, time=${task.__executedCpuTime}`);
|
|
349
|
+
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
*
|
|
354
|
+
* @param {Task} task
|
|
355
|
+
* @param {*} [reason]
|
|
356
|
+
*/
|
|
357
|
+
#fail_task(task, reason) {
|
|
311
358
|
const readyTasks = this.queueReady;
|
|
312
|
-
|
|
359
|
+
const taskIndex = readyTasks.indexOf(task);
|
|
313
360
|
|
|
314
|
-
|
|
315
|
-
|
|
361
|
+
if (taskIndex !== -1) {
|
|
362
|
+
readyTasks.splice(taskIndex, 1);
|
|
363
|
+
} else {
|
|
364
|
+
console.error("Failed to remove ready task, not found in the ready queue", task, readyTasks.slice());
|
|
316
365
|
}
|
|
317
366
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
367
|
+
task.state.set(TaskState.FAILED);
|
|
368
|
+
task.on.failed.send1(reason);
|
|
321
369
|
|
|
322
|
-
|
|
323
|
-
return readyTasks[0];
|
|
324
|
-
}
|
|
370
|
+
this.resolveTasks();
|
|
325
371
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
*/
|
|
329
|
-
let pickNextTask;
|
|
372
|
+
this.on.task_completed.send1(task);
|
|
373
|
+
}
|
|
330
374
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
pickNextTask = pickNextTaskSequential;
|
|
341
|
-
break;
|
|
342
|
-
}
|
|
375
|
+
/**
|
|
376
|
+
*
|
|
377
|
+
* @param {Task} task
|
|
378
|
+
* @param {number} time in milliseconds
|
|
379
|
+
* @param {function} completionCallback
|
|
380
|
+
* @param {function} failureCallback
|
|
381
|
+
*/
|
|
382
|
+
#runTaskForTimeMonitored(task, time) {
|
|
383
|
+
let cycle_count = 0;
|
|
343
384
|
|
|
344
385
|
/**
|
|
345
386
|
*
|
|
346
|
-
* @
|
|
387
|
+
* @type {function(): TaskSignal}
|
|
347
388
|
*/
|
|
348
|
-
|
|
389
|
+
const cycle = task.cycle;
|
|
349
390
|
|
|
350
|
-
|
|
391
|
+
const startTime = performance.now();
|
|
351
392
|
|
|
352
|
-
|
|
353
|
-
readyTasks.splice(taskIndex, 1);
|
|
354
|
-
} else {
|
|
355
|
-
console.error("Failed to remove ready task, not found in the ready queue", task, readyTasks.slice());
|
|
356
|
-
}
|
|
393
|
+
const endTime = startTime + time;
|
|
357
394
|
|
|
395
|
+
//We use tiny delta to avoid problems with timer accuracy on some systems that result in 0 measured execution time
|
|
396
|
+
let t = startTime + 0.000001;
|
|
358
397
|
|
|
359
|
-
|
|
360
|
-
task.on.completed.send1(self);
|
|
398
|
+
let signal;
|
|
361
399
|
|
|
362
|
-
|
|
363
|
-
|
|
400
|
+
while (t < endTime) {
|
|
401
|
+
cycle_count++;
|
|
364
402
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
403
|
+
signal = cycle();
|
|
404
|
+
t = performance.now();
|
|
368
405
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
406
|
+
if (signal === TaskSignal.Continue) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (signal === TaskSignal.Yield) {
|
|
411
|
+
//give up current quanta
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
376
414
|
|
|
377
|
-
if (
|
|
378
|
-
|
|
415
|
+
if (signal === TaskSignal.EndSuccess) {
|
|
416
|
+
break;
|
|
417
|
+
} else if (signal === TaskSignal.EndFailure) {
|
|
418
|
+
break;
|
|
379
419
|
} else {
|
|
380
|
-
|
|
420
|
+
throw new Error(`Task '${task.name}' produced unknown signal: ` + signal);
|
|
381
421
|
}
|
|
422
|
+
}
|
|
382
423
|
|
|
383
|
-
|
|
384
|
-
task.on.failed.dispatch(reason);
|
|
424
|
+
const executionDuration = t - startTime;
|
|
385
425
|
|
|
386
|
-
|
|
387
|
-
|
|
426
|
+
task.__executedCpuTime += executionDuration;
|
|
427
|
+
task.__executedCycleCount += cycle_count;
|
|
388
428
|
|
|
389
|
-
|
|
429
|
+
if (signal === TaskSignal.EndSuccess) {
|
|
430
|
+
this.#complete_task(task);
|
|
431
|
+
} else if (signal === TaskSignal.EndFailure) {
|
|
432
|
+
this.#fail_task(task, "Task signalled failure");
|
|
390
433
|
}
|
|
391
434
|
|
|
392
|
-
|
|
393
|
-
|
|
435
|
+
return executionDuration;
|
|
436
|
+
}
|
|
394
437
|
|
|
395
|
-
|
|
438
|
+
#bound_executeTimeSlice = this.#executeTimeSlice.bind(this);
|
|
396
439
|
|
|
397
|
-
|
|
398
|
-
|
|
440
|
+
#executeTimeSlice() {
|
|
441
|
+
let sliceTimeLeft = this.workTime;
|
|
399
442
|
|
|
400
|
-
|
|
401
|
-
console.warn('Next task not found, likely result of removing task mid-execution');
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
443
|
+
let executionTime = 0;
|
|
404
444
|
|
|
405
|
-
|
|
406
|
-
executionTime = runTaskForTimeMonitored(task, sliceTimeLeft, completeTask, failTask);
|
|
407
|
-
} catch (e) {
|
|
408
|
-
console.error(`Task threw an exception`, task, e);
|
|
409
|
-
failTask(task, e);
|
|
410
|
-
}
|
|
445
|
+
const queueReady = this.queueReady;
|
|
411
446
|
|
|
412
|
-
|
|
413
|
-
|
|
447
|
+
while (queueReady.length > 0) {
|
|
448
|
+
const task = this.#pick_next_task();
|
|
414
449
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
450
|
+
if (task === undefined) {
|
|
451
|
+
console.warn('Next task not found, likely result of removing task mid-execution');
|
|
452
|
+
break;
|
|
418
453
|
}
|
|
419
454
|
|
|
420
|
-
|
|
421
|
-
|
|
455
|
+
try {
|
|
456
|
+
executionTime = this.#runTaskForTimeMonitored(task, sliceTimeLeft);
|
|
457
|
+
} catch (e) {
|
|
458
|
+
console.error(`Task threw an exception`, task, e);
|
|
459
|
+
this.#fail_task(task, e);
|
|
460
|
+
}
|
|
422
461
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
462
|
+
this.#cycle_count++;
|
|
463
|
+
|
|
464
|
+
//make sure that execution time that we subtract from current CPU slice is always reducing the slice
|
|
465
|
+
sliceTimeLeft -= Math.max(executionTime, 1);
|
|
466
|
+
|
|
467
|
+
if (sliceTimeLeft <= 0) {
|
|
468
|
+
break;
|
|
429
469
|
}
|
|
430
470
|
}
|
|
431
471
|
|
|
432
|
-
|
|
472
|
+
if (this.queueReady.length === 0) {
|
|
473
|
+
this.busy = false;
|
|
474
|
+
this.on.completed.send0();
|
|
475
|
+
} else {
|
|
476
|
+
//schedule next time slice
|
|
477
|
+
this.#timeout_handle = setTimeout(this.#bound_executeTimeSlice, this.quietTime);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* kicks the scheduler into action, this is an internal method and should not be called from outside
|
|
483
|
+
* @private
|
|
484
|
+
*/
|
|
485
|
+
prod() {
|
|
486
|
+
this.resolveTasks();
|
|
433
487
|
|
|
434
|
-
if (!this.busy &&
|
|
488
|
+
if (!this.busy && this.queueReady.length > 0) {
|
|
435
489
|
this.busy = true;
|
|
436
|
-
executeTimeSlice();
|
|
490
|
+
this.#executeTimeSlice();
|
|
437
491
|
}
|
|
438
492
|
|
|
439
493
|
}
|
|
@@ -459,147 +513,6 @@ const SchedulingPolicy = {
|
|
|
459
513
|
|
|
460
514
|
ConcurrentExecutor.POLICY = SchedulingPolicy;
|
|
461
515
|
|
|
462
|
-
/**
|
|
463
|
-
*
|
|
464
|
-
* @param {Task} task
|
|
465
|
-
* @param {number} time in milliseconds
|
|
466
|
-
* @param {function} completionCallback
|
|
467
|
-
* @param {function} failureCallback
|
|
468
|
-
*/
|
|
469
|
-
function runTaskForTimeEstimated(task, time, completionCallback, failureCallback) {
|
|
470
|
-
let cycle_count = 0;
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
*
|
|
474
|
-
* @type {function(): TaskSignal}
|
|
475
|
-
*/
|
|
476
|
-
const cycle = task.cycle;
|
|
477
|
-
|
|
478
|
-
const startTime = performance.now();
|
|
479
|
-
|
|
480
|
-
let signal;
|
|
481
|
-
|
|
482
|
-
const cycle_time = task.__executedCpuTime / task.__executedCycleCount;
|
|
483
|
-
|
|
484
|
-
const estimated_cycles = clamp(Math.ceil(time / cycle_time), 1, 10000);
|
|
485
|
-
|
|
486
|
-
while (cycle_count < estimated_cycles) {
|
|
487
|
-
cycle_count++
|
|
488
|
-
|
|
489
|
-
signal = cycle();
|
|
490
|
-
|
|
491
|
-
if (signal === TaskSignal.Continue) {
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
if (signal === TaskSignal.Yield) {
|
|
496
|
-
//give up current quanta
|
|
497
|
-
break;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (signal === TaskSignal.EndSuccess) {
|
|
501
|
-
break;
|
|
502
|
-
} else if (signal === TaskSignal.EndFailure) {
|
|
503
|
-
break;
|
|
504
|
-
} else {
|
|
505
|
-
throw new Error(`Task '${task.name}' produced unknown signal: ` + signal);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
//We use tiny delta to avoid problems with timer accuracy on some systems that result in 0 measured execution time
|
|
510
|
-
const endTime = performance.now() + 0.000001;
|
|
511
|
-
|
|
512
|
-
const executionDuration = endTime - startTime;
|
|
513
|
-
|
|
514
|
-
task.__executedCpuTime += executionDuration;
|
|
515
|
-
task.__executedCycleCount += cycle_count;
|
|
516
|
-
|
|
517
|
-
if (signal === TaskSignal.EndSuccess) {
|
|
518
|
-
completionCallback(task);
|
|
519
|
-
} else if (signal === TaskSignal.EndFailure) {
|
|
520
|
-
failureCallback(task, "Task signalled failure");
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return executionDuration;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
*
|
|
528
|
-
* @param {Task} task
|
|
529
|
-
* @param {number} time in milliseconds
|
|
530
|
-
* @param {function} completionCallback
|
|
531
|
-
* @param {function} failureCallback
|
|
532
|
-
*/
|
|
533
|
-
function runTaskForTime2(task, time, completionCallback, failureCallback) {
|
|
534
|
-
if (task.__executedCycleCount > 1000) {
|
|
535
|
-
return runTaskForTimeEstimated(task, time, completionCallback, failureCallback);
|
|
536
|
-
} else {
|
|
537
|
-
return runTaskForTimeMonitored(task, time, completionCallback, failureCallback);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
*
|
|
543
|
-
* @param {Task} task
|
|
544
|
-
* @param {number} time in milliseconds
|
|
545
|
-
* @param {function} completionCallback
|
|
546
|
-
* @param {function} failureCallback
|
|
547
|
-
*/
|
|
548
|
-
function runTaskForTimeMonitored(task, time, completionCallback, failureCallback) {
|
|
549
|
-
let cycle_count = 0;
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
*
|
|
553
|
-
* @type {function(): TaskSignal}
|
|
554
|
-
*/
|
|
555
|
-
const cycle = task.cycle;
|
|
556
|
-
|
|
557
|
-
const startTime = performance.now();
|
|
558
|
-
|
|
559
|
-
const endTime = startTime + time;
|
|
560
|
-
|
|
561
|
-
//We use tiny delta to avoid problems with timer accuracy on some systems that result in 0 measured execution time
|
|
562
|
-
let t = startTime + 0.000001;
|
|
563
|
-
|
|
564
|
-
let signal;
|
|
565
|
-
|
|
566
|
-
while (t < endTime) {
|
|
567
|
-
cycle_count++;
|
|
568
|
-
|
|
569
|
-
signal = cycle();
|
|
570
|
-
t = performance.now();
|
|
571
|
-
|
|
572
|
-
if (signal === TaskSignal.Continue) {
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
if (signal === TaskSignal.Yield) {
|
|
577
|
-
//give up current quanta
|
|
578
|
-
break;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (signal === TaskSignal.EndSuccess) {
|
|
582
|
-
break;
|
|
583
|
-
} else if (signal === TaskSignal.EndFailure) {
|
|
584
|
-
break;
|
|
585
|
-
} else {
|
|
586
|
-
throw new Error(`Task '${task.name}' produced unknown signal: ` + signal);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const executionDuration = t - startTime;
|
|
591
|
-
|
|
592
|
-
task.__executedCpuTime += executionDuration;
|
|
593
|
-
task.__executedCycleCount += cycle_count;
|
|
594
|
-
|
|
595
|
-
if (signal === TaskSignal.EndSuccess) {
|
|
596
|
-
completionCallback(task);
|
|
597
|
-
} else if (signal === TaskSignal.EndFailure) {
|
|
598
|
-
failureCallback(task, "Task signalled failure");
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
return executionDuration;
|
|
602
|
-
}
|
|
603
516
|
|
|
604
517
|
const ResolutionType = {
|
|
605
518
|
READY: 0,
|
|
@@ -215,6 +215,17 @@ export class Blackboard extends AbstractBlackboard {
|
|
|
215
215
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
*
|
|
221
|
+
* @param {any} json
|
|
222
|
+
* @return {Blackboard}
|
|
223
|
+
*/
|
|
224
|
+
static fromJSON(json) {
|
|
225
|
+
const bb = new Blackboard();
|
|
226
|
+
bb.fromJSON(json);
|
|
227
|
+
return bb;
|
|
228
|
+
}
|
|
218
229
|
}
|
|
219
230
|
|
|
220
231
|
Blackboard.typeName = 'Blackboard';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import ParserError from "../../../../core/parser/simple/ParserError.js";
|
|
2
|
-
import { readLiteral } from "../../../../core/parser/simple/SimpleParser.js";
|
|
3
2
|
import { KeyValuePair } from "../../../../core/collection/KeyValuePair.js";
|
|
4
3
|
import Token from "../../../../core/parser/simple/Token.js";
|
|
5
4
|
import { TooltipTokenType } from "./TooltipTokenType.js";
|
|
5
|
+
import { readLiteralToken } from "../../../../core/parser/simple/readLiteralToken.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
*
|
|
@@ -41,7 +41,7 @@ export function readReferenceValueToken(text, cursor, length) {
|
|
|
41
41
|
throw new ParserError(i, `Input underflow while reading reference value, value so far='${text.substring(valueStart, length)}'`, text);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
const literal =
|
|
44
|
+
const literal = readLiteralToken(text, i, length);
|
|
45
45
|
|
|
46
46
|
i = literal.end;
|
|
47
47
|
|