diff --git a/.env b/.env index 78f4c7b3..da9bb1fb 100644 --- a/.env +++ b/.env @@ -5,3 +5,5 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 +MAX_STATES_EXTRACTION_CONCURRENT_TASKS=10 +DEBUG_TASKS=false \ No newline at end of file diff --git a/.github/workflows/angular-tests.yml b/.github/workflows/angular-tests.yml new file mode 100644 index 00000000..043e566a --- /dev/null +++ b/.github/workflows/angular-tests.yml @@ -0,0 +1,34 @@ +name: Angular Tests + +on: + push: + branches: + - main + paths: + - 'python/**' + pull_request: + branches: + - main + paths: + - 'python/**' + +jobs: + angular-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: '22' + + - name: Install dependencies + run: npm install + working-directory: ./multimodal-ui + + - name: Run tests + run: npm run test:ci + working-directory: ./multimodal-ui diff --git a/.gitignore b/.gitignore index 84cd01ff..2d60a2b9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ venv # Python cache __pycache__ +# Input data +data/ + # Data files (saved_logs, saved_simulations) output/saved_logs output/saved_simulations diff --git a/README.md b/README.md index 4ab07be7..65425a75 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,8 @@ The most useful environment variables are `CLIENT_PORT`, `SERVER_PORT`, `INPUT_D - `OUTPUT_DATA_DIRECTORY_PATH` (default `output`): The path to the output data directory. - `NUMBER_OF_UPDATES_BETWEEN_STATES` (default `1000`): The number of updates between simulation states. The lower the number, the larger the save file will be. However, increasing this number may cause the animation to freeze when receiving new states from the server. - `NUMBER_OF_STATES_TO_SEND_AT_ONCE` (default `1`): The number of states to send at once. Increasing this number may cause the animation to freeze when receiving new states from the server. +- `MAX_STATES_EXTRACTION_CONCURRENT_TASKS` (default `10`): The maximum number of states being extracted at the same time. Increasing this number might impact the memory usage during the loading only for a slight increase in performance. +- `DEBUG_TASKS` (default `false`): If set to `true`, debug logs regarding the tasks will be printed in the client console. ## Frontend diff --git a/multimodal-ui/angular.json b/multimodal-ui/angular.json index f583dd8d..212363c1 100644 --- a/multimodal-ui/angular.json +++ b/multimodal-ui/angular.json @@ -26,9 +26,6 @@ }, "@schematics/angular:resolver": { "skipTests": true - }, - "@schematics/angular:service": { - "skipTests": true } }, "root": "", @@ -114,6 +111,12 @@ "@angular/material/prebuilt-themes/azure-blue.css", "src/styles.scss" ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.test.ts" + } + ], "scripts": [] } }, diff --git a/multimodal-ui/package.json b/multimodal-ui/package.json index a041a7bf..75be5138 100644 --- a/multimodal-ui/package.json +++ b/multimodal-ui/package.json @@ -7,7 +7,8 @@ "build": "npm run set-env && ng build", "set-env": "ts-node scripts/set-environment.ts && prettier --write ./angular.json ./public/environment.json", "watch": "ng build --watch --configuration development", - "test": "ng test", + "test": "ng test --poll 1000", + "test:ci": "ng test --no-watch --no-progress --browsers=ChromeHeadless", "lint": "ng lint", "format": "prettier --write .", "format:check": "prettier --check ." diff --git a/multimodal-ui/public/environment.json b/multimodal-ui/public/environment.json index c3731a7d..2e0cf521 100644 --- a/multimodal-ui/public/environment.json +++ b/multimodal-ui/public/environment.json @@ -6,5 +6,7 @@ "OUTPUT_DATA_DIRECTORY_PATH": "output", "NUMBER_OF_UPDATES_BETWEEN_STATES": "1000", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", + "MAX_STATES_EXTRACTION_CONCURRENT_TASKS": "10", + "DEBUG_TASKS": "false", "HOST": "127.0.0.1" } diff --git a/multimodal-ui/src/app/interfaces/continuous.model.ts b/multimodal-ui/src/app/interfaces/continuous.model.ts index 0a01359b..8701f8de 100644 --- a/multimodal-ui/src/app/interfaces/continuous.model.ts +++ b/multimodal-ui/src/app/interfaces/continuous.model.ts @@ -6,12 +6,7 @@ import { SimulationState } from './state.model'; import { Statistics } from './statistics.model'; import { Stop } from './stop.model'; import { Tagged } from './tags.model'; -import { - AtomicTask, - BUILD_CONTINUOUS_ENVIRONMENT_TASK_PRIORITY as BUILD_CONTINUOUS_ENVIRONMENTS_TASK_PRIORITY, - CompositeTask, - Task, -} from './task.model'; +import { AtomicTask, CompositeTask, Task } from './task.model'; import { PassengerUpdate, StatisticsUpdate, @@ -112,42 +107,14 @@ export function createContinuousEnvironmentReferences(): ContinuousEnvironmentRe } // MARK: Tasks -export class BuildContinuousEnvironmentsTask extends CompositeTask { - private readonly continuousEnvironments: ContinuousEnvironment[] = []; - - constructor( - queue: SortedList, - private readonly states: SimulationState[], - private readonly references: ContinuousEnvironmentReferences, - private readonly callback: (environments: ContinuousEnvironment[]) => void, - ) { - super(BUILD_CONTINUOUS_ENVIRONMENTS_TASK_PRIORITY, queue); - } - - protected override beforeAll(): void { - for (const state of this.states) { - new BuildContinuousEnvironmentTask( - this.subtasks, - state, - this.references, - this.continuousEnvironments, - ).addToQueue(); - } - } - - protected override afterAll(): void { - this.callback(this.continuousEnvironments); - } -} - -class BuildContinuousEnvironmentTask extends CompositeTask { +export class BuildContinuousEnvironmentTask extends CompositeTask { private readonly continuousEnvironment: ContinuousEnvironment; constructor( queue: SortedList, private readonly state: SimulationState, private readonly references: ContinuousEnvironmentReferences, - private readonly continuousEnvironments: ContinuousEnvironment[], + private readonly callback: (environment: ContinuousEnvironment) => void, ) { super(0, queue); @@ -207,7 +174,7 @@ class BuildContinuousEnvironmentTask extends CompositeTask { } protected override afterAll(): void { - this.continuousEnvironments.push(this.continuousEnvironment); + this.callback(this.continuousEnvironment); } private buildEmptyContinuousEnvironment(): ContinuousEnvironment { diff --git a/multimodal-ui/src/app/interfaces/performances.model.ts b/multimodal-ui/src/app/interfaces/performances.model.ts index df87b0a5..6b7ed13a 100644 --- a/multimodal-ui/src/app/interfaces/performances.model.ts +++ b/multimodal-ui/src/app/interfaces/performances.model.ts @@ -13,6 +13,21 @@ export class SortedList { return this._items.shift(); } + public remove(item: T): void { + const startIndex = this.findFirstEqualIndex(item); + + if (startIndex === null) { + // Item not in list + return; + } + + const index = this._items.indexOf(item, startIndex); + + if (index !== -1) { + this._items.splice(index, 1); + } + } + public get length(): number { return this._items.length; } @@ -44,4 +59,36 @@ export class SortedList { return low; // Insertion point } + + private findFirstEqualIndex(item: T): number | null { + let low = 0; + let high = this._items.length - 1; + + while (low < high) { + const mid = Math.floor((low + high) / 2); + const comparison = this.compare(this._items[mid], item); + + if (comparison < 0) { + low = mid + 1; + } else if (comparison > 0) { + high = mid - 1; + } else { + high = mid; + } + } + + const lowComparison = this.compare(this._items[low], item); + + if (lowComparison === 0) { + return low; + } + + const highComparison = this.compare(this._items[high], item); + + if (highComparison === 0) { + return high; + } + + return null; + } } diff --git a/multimodal-ui/src/app/interfaces/state.model.ts b/multimodal-ui/src/app/interfaces/state.model.ts index 25a78be4..d6df0c50 100644 --- a/multimodal-ui/src/app/interfaces/state.model.ts +++ b/multimodal-ui/src/app/interfaces/state.model.ts @@ -18,100 +18,49 @@ export interface SimulationState extends SimulationEnvironment { } // MARK: Extract State + +interface ExtractStateTaskState { + environment: + | (SimulationEnvironment & Pick) + | null; + updates: Update[]; +} + export class ExtractStateTask extends CompositeTask { - private environments: (SimulationEnvironment & - Pick)[] = []; - private updatesByFirstUpdateIndex: Record = {}; + private state: ExtractStateTaskState = { + environment: null, + updates: [], + }; constructor( queue: SortedList, - private readonly serializedEnvironments: unknown, + private readonly serializedEnvironment: unknown, private readonly serializedUpdates: unknown, - private readonly callback: (states: SimulationState[] | null) => void, + private readonly callback: (state: SimulationState | null) => void, ) { super(EXTRACT_STATE_TASK_PRIORITY, queue); } public override beforeAll(): void { - new ExtractEnvironmentsTask( + new ExtractEnvironmentTask( this.subtasks, - this.serializedEnvironments, - this.environments, + this.serializedEnvironment, + this.state, ).addToQueue(); - new ExtractAllUpdatesTask( + new ExtractUpdatesTask( this.subtasks, this.serializedUpdates, - this.updatesByFirstUpdateIndex, + this.state, ).addToQueue(); } public override afterAll(): void { - const states: SimulationState[] = []; - - for (const environment of this.environments) { - const updates = this.updatesByFirstUpdateIndex[environment.updateIndex]; - - if (updates === undefined) { - console.error( - 'No updates found for environment', - environment, - 'in updates', - this.updatesByFirstUpdateIndex, - 'from serialized updates', - this.serializedUpdates, - 'and serialized environments', - this.serializedEnvironments, - ); - - // TODO #42 Failed - this.callback(null); - continue; - } - - states.push({ - ...environment, - updates, - }); - } - - this.callback(states); - } -} - -// MARK: Extract Environments -class ExtractEnvironmentsTask extends CompositeTask { - constructor( - queue: SortedList, - private readonly serializedEnvironments: unknown, - private readonly environments: (SimulationEnvironment & - Pick)[], - ) { - super(EXTRACT_STATE_TASK_PRIORITY, queue); - } - - protected override beforeAll(): void { - if (!Array.isArray(this.serializedEnvironments)) { - console.error( - 'Invalid data type for serialized environments', - this.serializedEnvironments, - ); - - // TODO #42 Failed - return; - } - - for (const serializedEnvironment of this.serializedEnvironments) { - new ExtractEnvironmentTask( - this.subtasks, - serializedEnvironment, - this.environments, - ).addToQueue(); - } - } - - protected override afterAll(): void { - // Nothing to do + this.callback( + this.state.environment + ? { ...this.state.environment, updates: this.state.updates } + : null, + ); } } @@ -130,8 +79,7 @@ class ExtractEnvironmentTask extends CompositeTask { constructor( queue: SortedList, private readonly serializedEnvironment: unknown, - private readonly environments: (SimulationEnvironment & - Pick)[], + private readonly state: ExtractStateTaskState, ) { super(EXTRACT_STATE_TASK_PRIORITY, queue); } @@ -263,7 +211,7 @@ class ExtractEnvironmentTask extends CompositeTask { } protected override afterAll(): void { - this.environments.push(this.environment); + this.state.environment = this.environment; } } @@ -339,45 +287,6 @@ class ExtractVehicleTask extends Task { } } -// MARK: Extract All Updates -class ExtractAllUpdatesTask extends CompositeTask { - constructor( - queue: SortedList, - private readonly serializedUpdates: unknown, - private readonly updatesByFirstUpdateIndex: Record, - ) { - super(EXTRACT_STATE_TASK_PRIORITY, queue); - } - - protected override beforeAll(): void { - if ( - typeof this.serializedUpdates !== 'object' || - this.serializedUpdates === null - ) { - console.error( - 'Invalid data type for serialized updates', - this.serializedUpdates, - ); - - // TODO #42 Failed - return; - } - - for (const [key, value] of Object.entries(this.serializedUpdates)) { - new ExtractUpdatesTask( - this.subtasks, - key, - value, - this.updatesByFirstUpdateIndex, - ).addToQueue(); - } - } - - protected override afterAll(): void { - // Nothing to do - } -} - // MARK: Extract Updates class ExtractUpdatesTask extends CompositeTask { private updates: SortedList = new SortedList( @@ -386,54 +295,23 @@ class ExtractUpdatesTask extends CompositeTask { constructor( queue: SortedList, - private readonly serializedUpdatesKey: unknown, private readonly serializedUpdates: unknown, - private readonly updatesByFirstUpdateIndex: Record, + private readonly state: ExtractStateTaskState, ) { super(EXTRACT_STATE_TASK_PRIORITY, queue); } beforeAll(): void { - if (typeof this.serializedUpdatesKey !== 'string') { - console.error( - 'Invalid data type for key', - this.serializedUpdatesKey, - 'with serialized updates', - this.serializedUpdates, - ); - - // TODO #42 Failed - return; - } - - const key = parseInt(this.serializedUpdatesKey); - - if (isNaN(key)) { - console.error( - 'Key is not a valid number', - this.serializedUpdatesKey, - 'with serialized updates', - this.serializedUpdates, - ); - - // TODO #42 Failed - return; - } - if (!Array.isArray(this.serializedUpdates)) { console.error( 'Invalid data type for serialized updates', this.serializedUpdates, - 'with key', - this.serializedUpdatesKey, ); // TODO #42 Failed return; } - this.updatesByFirstUpdateIndex[key] = this.updates.editableItems; - for (const serializedUpdate of this.serializedUpdates) { new ExtractUpdateTask( this.subtasks, @@ -445,6 +323,7 @@ class ExtractUpdatesTask extends CompositeTask { protected override afterAll(): void { // Nothing to do + this.state.updates = this.updates.editableItems; } } diff --git a/multimodal-ui/src/app/interfaces/task.model.ts b/multimodal-ui/src/app/interfaces/task.model.ts index 55bb2eca..f61e8792 100644 --- a/multimodal-ui/src/app/interfaces/task.model.ts +++ b/multimodal-ui/src/app/interfaces/task.model.ts @@ -1,7 +1,7 @@ import { SortedList } from './performances.model'; export const EXTRACT_STATE_TASK_PRIORITY = 1; -export const BUILD_CONTINUOUS_ENVIRONMENT_TASK_PRIORITY = 2; +export const BUILD_CONTINUOUS_ENVIRONMENT_TASK_PRIORITY = 1; export function emptyTaskQueue(): SortedList { return new SortedList((a, b) => b.priority - a.priority); @@ -13,13 +13,18 @@ export function emptyTaskQueue(): SortedList { * Tasks should be quick to process. */ export abstract class Task { + protected _priority: number; + constructor( - public readonly priority: number, + priority: number, protected readonly queue: SortedList, - ) {} + ) { + this._priority = priority; + } - public addToQueue(): void { + public addToQueue(this: T): T { this.queue.add(this); + return this; } /** @@ -33,6 +38,19 @@ export abstract class Task { public get numberOfTasks(): number { return 1; } + + public get priority(): number { + return this._priority; + } + + public updatePriority(newPriority: number): void { + if (this._priority !== newPriority) { + this.queue.remove(this); + + this._priority = newPriority; + this.addToQueue(); + } + } } /** diff --git a/multimodal-ui/src/app/services/animation.service.ts b/multimodal-ui/src/app/services/animation.service.ts index d21dbb2a..fffdc62d 100644 --- a/multimodal-ui/src/app/services/animation.service.ts +++ b/multimodal-ui/src/app/services/animation.service.ts @@ -70,9 +70,12 @@ export class AnimationService { private readonly SAFETY_RATIO = 0.95; // Safety ratio to ensure we don't exceed the frame time private readonly MIN_FRAME_RATE = 60; // This can be increased at the cost of a longer load time. private readonly MIN_TASK_ALLOCATION = 5; + private readonly MIN_TASK_ALLOCATION_PAUSED = 25; private readonly MIN_TIME_PER_FRAME = (1000 / this.MIN_FRAME_RATE) * this.SAFETY_RATIO; + private taskAllocationMode: 'normal' | 'paused' = 'normal'; + // MARK: Properties // Use two variables to avoid changing the ones used for the animation during the animation @@ -338,7 +341,10 @@ export class AnimationService { this.taskService.processTasks( Math.max( lastRedrawTime + this.MIN_TIME_PER_FRAME, - now + this.MIN_TASK_ALLOCATION, + now + + (this.taskAllocationMode === 'normal' + ? this.MIN_TASK_ALLOCATION + : this.MIN_TASK_ALLOCATION_PAUSED), ), ); @@ -367,8 +373,13 @@ export class AnimationService { return; // No polylines available } + const previousTime = this.animationVisualizationTime; + this.updateAnimationTime(); + this.taskAllocationMode = + this.animationVisualizationTime !== previousTime ? 'normal' : 'paused'; + const environment = this.updateEnvironment(); if (environment === null) { diff --git a/multimodal-ui/src/app/services/simulation.service.spec.ts b/multimodal-ui/src/app/services/simulation.service.spec.ts new file mode 100644 index 00000000..396754ed --- /dev/null +++ b/multimodal-ui/src/app/services/simulation.service.spec.ts @@ -0,0 +1,185 @@ +import { TestBed } from '@angular/core/testing'; +import { SortedList } from '../interfaces/performances.model'; +import { + AtomicTask, + emptyTaskQueue, + EXTRACT_STATE_TASK_PRIORITY, + Task, +} from '../interfaces/task.model'; +import { CommunicationService } from './communication.service'; +import { DataService } from './data.service'; +import { SimulationService, StateExtractionTask } from './simulation.service'; +import { TaskService } from './task.service'; +import { TimerService } from './timer.service'; + +function createTestTask(queue: SortedList, priority = 0): Task { + // eslint-disable-next-line @typescript-eslint/no-empty-function + return new AtomicTask(priority, queue, () => {}).addToQueue(); +} + +describe('SimulationService', () => { + let service: SimulationService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: DataService, useValue: {} }, + { provide: CommunicationService, useValue: {} }, + { provide: TaskService, useValue: {} }, + { provide: TimerService, useValue: {} }, + ], + }); + + service = TestBed.inject(SimulationService); + }); + + describe('updateTasksPriority', () => { + let wantedVisualizationTime: number; + + let taskAfter1: StateExtractionTask; + let taskAfter2: StateExtractionTask; + + let taskBefore1: StateExtractionTask; + let taskBefore2: StateExtractionTask; + + let taskEqual1: StateExtractionTask; + let taskEqual2: StateExtractionTask; + + let queue: SortedList; + + beforeEach(() => { + wantedVisualizationTime = 50; + + queue = emptyTaskQueue(); + + taskBefore1 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime - 2, + startUpdateIndex: 0, + }; + taskBefore2 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime - 1, + startUpdateIndex: 1, + }; + + taskEqual1 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime, + startUpdateIndex: 2, + }; + taskEqual2 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime, + startUpdateIndex: 3, + }; + + taskAfter1 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime + 1, + startUpdateIndex: 4, + }; + taskAfter2 = { + task: createTestTask(queue, EXTRACT_STATE_TASK_PRIORITY), + startTimestamp: wantedVisualizationTime + 2, + startUpdateIndex: 5, + }; + }); + + describe('when all tasks are after the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(1, taskAfter1); + service['stateExtractionTasks'].set(2, taskAfter2); + }); + + it('should make the closest task the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + + describe('when all tasks are before the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(2, taskBefore1); + service['stateExtractionTasks'].set(1, taskBefore2); + }); + + it('should make the closest task the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskBefore2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + }); + }); + + describe('when there are tasks before and after the wanted visualization time and the closest task is the one after', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(1, taskBefore1); + service['stateExtractionTasks'].set(2, taskAfter1); + }); + + it('should make the task before the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + + describe('when there are tasks before and after the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(2, taskBefore1); + service['stateExtractionTasks'].set(1, taskBefore2); + service['stateExtractionTasks'].set(3, taskAfter1); + service['stateExtractionTasks'].set(4, taskAfter2); + }); + + it('should make the closest task before the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskBefore2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskAfter2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + + describe('when there are tasks with the same start time as the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(1, taskEqual1); + service['stateExtractionTasks'].set(2, taskEqual2); + }); + + it('should make the task with the greatest update index the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskEqual1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskEqual2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + }); + }); + + describe('when there are tasks before, after and at the wanted visualization time', () => { + beforeEach(() => { + service['stateExtractionTasks'].set(2, taskBefore1); + service['stateExtractionTasks'].set(1, taskBefore2); + service['stateExtractionTasks'].set(1, taskEqual1); + service['stateExtractionTasks'].set(2, taskEqual2); + service['stateExtractionTasks'].set(3, taskAfter1); + service['stateExtractionTasks'].set(4, taskAfter2); + }); + + it('should make the task at the wanted visualization time with the greatest update index the most urgent', () => { + service['updateTasksPriority'](wantedVisualizationTime); + + expect(taskBefore1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskBefore2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskEqual1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskEqual2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY + 1); + expect(taskAfter1.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + expect(taskAfter2.task.priority).toBe(EXTRACT_STATE_TASK_PRIORITY); + }); + }); + }); +}); diff --git a/multimodal-ui/src/app/services/simulation.service.ts b/multimodal-ui/src/app/services/simulation.service.ts index b0ba31dd..417dbac3 100644 --- a/multimodal-ui/src/app/services/simulation.service.ts +++ b/multimodal-ui/src/app/services/simulation.service.ts @@ -1,10 +1,15 @@ import { computed, + effect, Injectable, signal, Signal, WritableSignal, } from '@angular/core'; +import { + DEBUG_TASKS, + MAX_STATES_EXTRACTION_CONCURRENT_TASKS, +} from '../../environments/environment'; import { ContinuousEnvironment, ContinuousEnvironmentReferences, @@ -17,9 +22,11 @@ import { } from '../interfaces/polylines.model'; import { Simulation } from '../interfaces/simulation.model'; import { SimulationState } from '../interfaces/state.model'; +import { EXTRACT_STATE_TASK_PRIORITY, Task } from '../interfaces/task.model'; import { CommunicationService } from './communication.service'; import { DataService } from './data.service'; import { TaskService } from './task.service'; +import { TimerService } from './timer.service'; interface DebounceSettings { readonly debounceTime: number; @@ -27,21 +34,32 @@ interface DebounceSettings { timeoutId: ReturnType | null; } +export interface StateExtractionTask { + task: Task; + startTimestamp: number; + startUpdateIndex: number; +} + @Injectable({ providedIn: 'root', }) export class SimulationService { // MARK: Properties + private readonly stateExtractionTasks = new Map< + number, + StateExtractionTask + >(); + private readonly _activeSimulationIdSignal: WritableSignal = signal(null); private readonly _simulationPolylinesSignal: WritableSignal = signal(null); - private readonly _isFetchingStatesSignal: WritableSignal = + private readonly isFetchingStatesSignal: WritableSignal = signal(false); - private readonly _isFetchingPolylinesSignal: WritableSignal = + private readonly isFetchingPolylinesSignal: WritableSignal = signal(false); private readonly _continuousEnvironmentsSignal: WritableSignal< @@ -53,7 +71,7 @@ export class SimulationService { private references: ContinuousEnvironmentReferences = createContinuousEnvironmentReferences(); - private readonly _hasAllStatesSignal: WritableSignal = signal(false); + private readonly hasAllStatesSignal: WritableSignal = signal(false); private readonly getPolylinesDebounceSettings: DebounceSettings = { debounceTime: 500, @@ -63,17 +81,35 @@ export class SimulationService { private readonly getMissingSimulationStatesDebounceSettings: DebounceSettings = { - debounceTime: 500, + debounceTime: 100, lastExecutionTime: null, timeoutId: null, }; + private wantedVisualizationTimeSignal: WritableSignal = + signal(null); + // MARK: Constructor constructor( private readonly dataService: DataService, private readonly communicationService: CommunicationService, private readonly taskService: TaskService, - ) {} + private readonly timerService: TimerService, + ) { + effect(() => this.getPolylinesIfNeeded()); + + effect(() => this.getMissingSimulationStatesIfNeeded()); + + effect( + () => (this.timerService.simulation = this.activeSimulationSignal()), + ); + + effect( + () => + (this.timerService.continuousEnvironments = + this.continuousEnvironmentsSignal()), + ); + } // MARK: Active simulation setActiveSimulationId(simulationId: string) { @@ -83,14 +119,9 @@ export class SimulationService { this.communicationService.on( 'missing-simulation-states', - ( - serializedMissingStatesEnvironments, - serializedMissingStatesUpdates, - hasAllStates, - ) => { + (serializedMissingStatesEnvironments, hasAllStates) => { this.onMissingSimulationStates( serializedMissingStatesEnvironments, - serializedMissingStatesUpdates, hasAllStates, ); }, @@ -99,7 +130,7 @@ export class SimulationService { this.communicationService.on( `polylines-${simulationId}`, (polylinesByCoordinates, version) => { - this._isFetchingPolylinesSignal.set(false); + this.isFetchingPolylinesSignal.set(false); this._simulationPolylinesSignal.set( extractAllPolylines(polylinesByCoordinates, version), @@ -115,8 +146,8 @@ export class SimulationService { this._simulationPolylinesSignal.set(null); - this._isFetchingStatesSignal.set(false); - this._isFetchingPolylinesSignal.set(false); + this.isFetchingStatesSignal.set(false); + this.isFetchingPolylinesSignal.set(false); this.communicationService.removeAllListeners('missing-simulation-states'); @@ -132,7 +163,7 @@ export class SimulationService { this.references = createContinuousEnvironmentReferences(); - this._hasAllStatesSignal.set(false); + this.hasAllStatesSignal.set(false); } get activeSimulationSignal(): Signal { @@ -176,26 +207,126 @@ export class SimulationService { ); } - getMissingSimulationStates( - simulationId: string, - visualizationTime: number, - completeStateUpdateIndexes: number[], - ) { - return this.runWithDebounce(() => { - this.getMissingSimulationStatesWithDebounce( - simulationId, - visualizationTime, - completeStateUpdateIndexes, + // MARK: States + private getMissingSimulationStatesIfNeeded() { + const hasAllStates = this.hasAllStatesSignal(); + + if (hasAllStates) { + return; + } + + const simulation = this.activeSimulationSignal(); + + if (simulation === null) { + this.timerService.isLoading = true; + return; + } + + const wantedVisualizationTime = this.wantedVisualizationTimeSignal(); + + if (wantedVisualizationTime === null) { + this.timerService.isLoading = true; + return; + } + + this.updateTasksPriority(wantedVisualizationTime); + + const isFetching = this.isFetchingStatesSignal(); + + if (isFetching) { + return; + } + + const continuousEnvironments = this.continuousEnvironmentsSignal(); + + if ( + this.stateExtractionTasks.size >= MAX_STATES_EXTRACTION_CONCURRENT_TASKS + ) { + return; + } + + const completeStateUpdateIndexes = continuousEnvironments + .filter((continuousEnvironment) => continuousEnvironment.isComplete) + .map((continuousEnvironment) => continuousEnvironment.startUpdateIndex); + + const currentlyProcessingStateUpdateIndexes = Array.from( + this.stateExtractionTasks.keys(), + ); + + this.runWithDebounce(() => { + this.getMissingSimulationStates( + simulation.id, + wantedVisualizationTime, + Array.from( + new Set( + completeStateUpdateIndexes.concat( + currentlyProcessingStateUpdateIndexes, + ), + ), + ), ); }, this.getMissingSimulationStatesDebounceSettings); } - private getMissingSimulationStatesWithDebounce( + private updateTasksPriority(wantedVisualizationTime: number) { + let mostUrgentTask: StateExtractionTask | null = null; + + let previousProximity = Infinity; + + for (const task of this.stateExtractionTasks.values()) { + task.task.updatePriority(EXTRACT_STATE_TASK_PRIORITY); + + const proximity = task.startTimestamp - wantedVisualizationTime; + + /** + * The most urgent task is the one with a start time before the wanted + * visualization time that is the closest to it. + * + * If all tasks are after the wanted visualization time, then the most + * urgent task is the one with the closest start time. + **/ + if ( + (proximity < 0 && + (previousProximity > 0 || proximity > previousProximity)) || + (proximity > 0 && proximity < previousProximity) || + (proximity === 0 && + (previousProximity !== 0 || + (mostUrgentTask?.startUpdateIndex ?? -Infinity) < + task.startUpdateIndex)) + ) { + mostUrgentTask = task; + + previousProximity = proximity; + } + } + + if (mostUrgentTask) { + mostUrgentTask.task.updatePriority(EXTRACT_STATE_TASK_PRIORITY + 1); + } + + if (DEBUG_TASKS) { + console.debug('updateTasksPriority', { + wantedVisualizationTime, + mostUrgentTask, + stateExtractionTasks: Array.from(this.stateExtractionTasks.entries()), + }); + } + } + + private getMissingSimulationStates( simulationId: string, visualizationTime: number, completeStateUpdateIndexes: number[], ) { - this._isFetchingStatesSignal.set(true); + if (DEBUG_TASKS) { + console.debug('getMissingSimulationStates', { + simulationId, + visualizationTime, + completeStateUpdateIndexes, + }); + } + + this.isFetchingStatesSignal.set(true); this.communicationService.emit( 'get-missing-simulation-states', @@ -205,95 +336,240 @@ export class SimulationService { ); } - getPolylines(simulationId: string) { - this.runWithDebounce(() => { - this.getPolylinesWithoutDebounce(simulationId); - }, this.getPolylinesDebounceSettings); + set wantedVisualizationTime(visualizationTime: number | null) { + this.wantedVisualizationTimeSignal.set(visualizationTime); } - private getPolylinesWithoutDebounce(simulationId: string) { - this._isFetchingPolylinesSignal.set(true); - - this.communicationService.emit('get-polylines', simulationId); + get continuousEnvironmentsSignal(): Signal { + return this._continuousEnvironmentsSignal; } - get simulationPolylinesSignal(): Signal { - return this._simulationPolylinesSignal; - } + // MARK: Polylines + private getPolylinesIfNeeded() { + const simulation = this.activeSimulationSignal(); - get isFetchingStatesSignal(): Signal { - return this._isFetchingStatesSignal; - } + if (simulation === null) { + return; + } + + const polylines = this.simulationPolylinesSignal(); + const isFetching = this.isFetchingPolylinesSignal(); - get isFetchingPolylinesSignal(): Signal { - return this._isFetchingPolylinesSignal; + const needPolylineUpdate = + polylines === null || polylines.version !== simulation.polylinesVersion; + + if (needPolylineUpdate && !isFetching) { + this.runWithDebounce(() => { + this.getPolylinesWithoutDebounce(simulation.id); + }, this.getPolylinesDebounceSettings); + } } - get continuousEnvironmentsSignal(): Signal { - return this._continuousEnvironmentsSignal; + private getPolylinesWithoutDebounce(simulationId: string) { + this.isFetchingPolylinesSignal.set(true); + + this.communicationService.emit('get-polylines', simulationId); } - get hasAllStatesSignal(): Signal { - return this._hasAllStatesSignal; + get simulationPolylinesSignal(): Signal { + return this._simulationPolylinesSignal; } // MARK: Event handlers private onMissingSimulationStates( - serializedMissingStatesEnvironments: unknown, - serializedMissingStatesUpdates: unknown, + serializedMissingStates: unknown, hasAllStates: unknown, ): void { - this.taskService.extractStateTask( - serializedMissingStatesEnvironments, - serializedMissingStatesUpdates, - (extractedStates) => - this.afterExtractStateTask(extractedStates, hasAllStates), - ); + if (DEBUG_TASKS) { + console.debug('onMissingSimulationStates', { + serializedMissingStates, + hasAllStates, + }); + } + + this.isFetchingStatesSignal.set(false); + + if (!Array.isArray(serializedMissingStates)) { + console.error( + 'Received invalid serializedMissingStates value from the server.', + ); + return; + } + + for (const serializedMissingState of serializedMissingStates as unknown[]) { + if ( + typeof serializedMissingState !== 'object' || + serializedMissingState === null + ) { + console.error( + 'Received invalid serializedMissingState value from the server.', + ); + return; + } + + if (!('environment' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing environment.', + ); + return; + } + + if (!('updates' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing updates.', + ); + return; + } + + if (!('startTimestamp' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing startTimestamp.', + ); + return; + } + + if (typeof serializedMissingState.startTimestamp !== 'number') { + console.error( + 'Received invalid serializedMissingState value from the server. Invalid startTimestamp.', + ); + return; + } + + if (!('startUpdateIndex' in serializedMissingState)) { + console.error( + 'Received invalid serializedMissingState value from the server. Missing startUpdateIndex.', + ); + return; + } + + if (typeof serializedMissingState.startUpdateIndex !== 'number') { + console.error( + 'Received invalid serializedMissingState value from the server. Invalid startUpdateIndex.', + ); + return; + } + + const startTimestamp = serializedMissingState.startTimestamp; + const startUpdateIndex = serializedMissingState.startUpdateIndex; + + const task = this.taskService.extractStateTask( + serializedMissingState.environment, + serializedMissingState.updates, + (extractedState) => + this.afterExtractStateTask( + extractedState, + startUpdateIndex, + startTimestamp, + ), + ); + + this.stateExtractionTasks.set(startUpdateIndex, { + task, + startTimestamp, + startUpdateIndex, + }); + + if (DEBUG_TASKS) { + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); + } + } + + if (typeof hasAllStates !== 'boolean') { + console.error('Received invalid hasAllStates value from the server.'); + return; + } + + this.hasAllStatesSignal.set(hasAllStates); } private afterExtractStateTask( - extractedStates: SimulationState[] | null, - hasAllStates: unknown, + extractedState: SimulationState | null, + startUpdateIndex: number, + startTimestamp: number, ): void { - if (extractedStates === null) { + if (DEBUG_TASKS) { + console.debug('afterExtractStateTask', { + extractedState, + startUpdateIndex, + startTimestamp, + }); + } + + if (extractedState === null) { console.error( - 'Failed to extract missing simulation states from the server response.', + 'Failed to extract missing simulation state from the server response.', ); - this._isFetchingStatesSignal.set(false); + this.stateExtractionTasks.delete(startUpdateIndex); + + if (DEBUG_TASKS) { + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); + } + return; } - this.taskService.buildContinuousEnvironmentsTask( - extractedStates, - this.references, - (continuousEnvironments) => - this.afterBuildContinuousEnvironmentsTask( - continuousEnvironments, - hasAllStates, - ), - ); + this.stateExtractionTasks.set(startUpdateIndex, { + task: this.taskService.buildContinuousEnvironmentTask( + extractedState, + this.references, + (continuousEnvironment) => + this.afterBuildContinuousEnvironmentTask( + continuousEnvironment, + startUpdateIndex, + ), + ), + startTimestamp, + startUpdateIndex, + }); + + if (DEBUG_TASKS) { + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); + } } - private afterBuildContinuousEnvironmentsTask( - continuousEnvironments: ContinuousEnvironment[], - hasAllStates: unknown, + private afterBuildContinuousEnvironmentTask( + continuousEnvironment: ContinuousEnvironment, + startUpdateIndex: number, ) { + if (DEBUG_TASKS) { + console.debug('afterBuildContinuousEnvironmentTask', { + continuousEnvironment, + startUpdateIndex, + }); + } + + this.stateExtractionTasks.delete(startUpdateIndex); + + if (DEBUG_TASKS) { + console.debug( + 'stateExtractionTasks', + Array.from(this.stateExtractionTasks.entries()), + ); + } + this._continuousEnvironmentsSignal.update((environments) => { const newEnvironments = [...environments]; - for (const environment of continuousEnvironments) { - const existingEnvironmentIndex = newEnvironments.findIndex( - (existingEnvironment) => - existingEnvironment.startUpdateIndex === - environment.startUpdateIndex, - ); + const existingEnvironmentIndex = newEnvironments.findIndex( + (existingEnvironment) => + existingEnvironment.startUpdateIndex === + continuousEnvironment.startUpdateIndex, + ); - if (existingEnvironmentIndex === -1) { - newEnvironments.push(environment); - } else { - newEnvironments[existingEnvironmentIndex] = environment; - } + if (existingEnvironmentIndex === -1) { + newEnvironments.push(continuousEnvironment); + } else { + newEnvironments[existingEnvironmentIndex] = continuousEnvironment; } newEnvironments.sort((a, b) => a.startUpdateIndex - b.startUpdateIndex); @@ -305,16 +581,6 @@ export class SimulationService { return newEnvironments; }); - - if (typeof hasAllStates !== 'boolean') { - console.error('Received invalid hasAllStates value from the server.'); - - hasAllStates = false; - } else { - this._hasAllStatesSignal.set(hasAllStates); - } - - this._isFetchingStatesSignal.set(false); } private runWithDebounce( diff --git a/multimodal-ui/src/app/services/task.service.ts b/multimodal-ui/src/app/services/task.service.ts index f7e4e0c9..a1654e95 100644 --- a/multimodal-ui/src/app/services/task.service.ts +++ b/multimodal-ui/src/app/services/task.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { - BuildContinuousEnvironmentsTask, + BuildContinuousEnvironmentTask, ContinuousEnvironment, ContinuousEnvironmentReferences, } from '../interfaces/continuous.model'; @@ -40,24 +40,24 @@ export class TaskService { // MARK: Specific Tasks extractStateTask( - serializedEnvironments: unknown, + serializedEnvironment: unknown, serializedUpdates: unknown, - callback: (states: SimulationState[] | null) => void, - ): void { - new ExtractStateTask( + callback: (states: SimulationState | null) => void, + ): ExtractStateTask { + return new ExtractStateTask( this.queue, - serializedEnvironments, + serializedEnvironment, serializedUpdates, callback, ).addToQueue(); } - buildContinuousEnvironmentsTask( - states: SimulationState[], + buildContinuousEnvironmentTask( + states: SimulationState, references: ContinuousEnvironmentReferences, - callback: (environments: ContinuousEnvironment[]) => void, - ) { - new BuildContinuousEnvironmentsTask( + callback: (environment: ContinuousEnvironment) => void, + ): BuildContinuousEnvironmentTask { + return new BuildContinuousEnvironmentTask( this.queue, states, references, diff --git a/multimodal-ui/src/app/services/timer.service.ts b/multimodal-ui/src/app/services/timer.service.ts index 45413a7b..37b2dd25 100644 --- a/multimodal-ui/src/app/services/timer.service.ts +++ b/multimodal-ui/src/app/services/timer.service.ts @@ -20,8 +20,10 @@ import { setVisualizationSpeedPowerLocalStorage, setVisualizationTimeLocalStorage, } from '../interfaces/local-storage'; -import { RUNNING_SIMULATION_STATUSES } from '../interfaces/simulation.model'; -import { SimulationService } from './simulation.service'; +import { + RUNNING_SIMULATION_STATUSES, + Simulation, +} from '../interfaces/simulation.model'; @Injectable({ providedIn: 'root', @@ -32,7 +34,8 @@ export class TimerService { private readonly MAX_TIME_STEP = 1 / this.MIN_FRAME_RATE; // seconds private _visualizationTime: number | null = null; private isEnvironmentLoaded = false; - private _continuousEnvironments: ContinuousEnvironment[] = []; + + continuousEnvironments: ContinuousEnvironment[] = []; private lastUpdateTime: number | null = null; @@ -50,6 +53,9 @@ export class TimerService { private readonly _directionSignal: WritableSignal = signal(null); + private readonly simulationSignal: WritableSignal = + signal(null); + readonly isPausedSignal: Signal = computed( () => this._isPausedSignal() === true, ); @@ -75,38 +81,38 @@ export class TimerService { }); // MARK: Constructor - constructor(private readonly simulationService: SimulationService) { + constructor() { effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.load(activeSimulation.id); + if (simulation !== null) { + this.load(simulation.id); } else { this.reset(); } }); effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.saveIsPaused(activeSimulation.id); + if (simulation !== null) { + this.saveIsPaused(simulation.id); } }); effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.saveSpeedPower(activeSimulation.id); + if (simulation !== null) { + this.saveSpeedPower(simulation.id); } }); effect(() => { - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); - if (activeSimulation !== null) { - this.saveDirection(activeSimulation.id); + if (simulation !== null) { + this.saveDirection(simulation.id); } }); } @@ -128,15 +134,15 @@ export class TimerService { this.nextDirection = value; } + set simulation(value: Simulation | null) { + this.simulationSignal.set(value); + } + // MARK: Getters get visualizationTime(): number | null { return this._visualizationTime; } - get continuousEnvironments(): ContinuousEnvironment[] { - return this._continuousEnvironments; - } - // MARK: Time Update /** * @@ -167,30 +173,25 @@ export class TimerService { visualizationTimeOverride, ); - const activeSimulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); if ( visualizationTime === null || elapsedTime === null || - activeSimulation === null + simulation === null ) { this.isEnvironmentLoaded = false; this._visualizationTime = null; - if (activeSimulation !== null) { - this.saveTime(activeSimulation.id); + if (simulation !== null) { + this.saveTime(simulation.id); } return null; } - const continuousEnvironments = - this.simulationService.continuousEnvironmentsSignal(); - - this._continuousEnvironments = continuousEnvironments; - const closestContinuousEnvironment = findClosestContinuousEnvironment( - continuousEnvironments, + this.continuousEnvironments, visualizationTime, ); @@ -198,7 +199,7 @@ export class TimerService { this._visualizationTime = visualizationTime; - this.saveTime(activeSimulation.id); + this.saveTime(simulation.id); return elapsedTime; } @@ -207,7 +208,7 @@ export class TimerService { visualizationTime: number | null; elapsedTime: number | null; } { - const simulation = this.simulationService.activeSimulationSignal(); + const simulation = this.simulationSignal(); if (simulation === null) { this.lastUpdateTime = null; @@ -276,7 +277,7 @@ export class TimerService { this._directionSignal.set(null); this._visualizationTime = null; this.isEnvironmentLoaded = false; - this._continuousEnvironments = []; + this.continuousEnvironments = []; this.lastUpdateTime = null; } diff --git a/multimodal-ui/src/app/services/visualization.service.ts b/multimodal-ui/src/app/services/visualization.service.ts index 804d0e75..9c292d60 100644 --- a/multimodal-ui/src/app/services/visualization.service.ts +++ b/multimodal-ui/src/app/services/visualization.service.ts @@ -76,73 +76,15 @@ export class VisualizationService { private readonly animationService: AnimationService, private readonly timerService: TimerService, ) { - effect(() => { - const environmentSlice = this.environmentSignal(); - this.environmentSlice = environmentSlice; - }); - - effect(() => { - const hasAllStates = this.simulationService.hasAllStatesSignal(); - - if (hasAllStates) { - this.timerService.isLoading = false; - return; - } - - const simulation = this.simulationService.activeSimulationSignal(); - - if (simulation === null) { - this.timerService.isLoading = true; - return; - } - - const wantedVisualizationTime = this._wantedVisualizationTimeSignal(); - - if (wantedVisualizationTime === null) { - this.timerService.isLoading = true; - return; - } - - const isFetching = this.simulationService.isFetchingStatesSignal(); - - if (!isFetching) { - const continuousEnvironments = - this.simulationService.continuousEnvironmentsSignal(); - const completeStateUpdateIndexes = continuousEnvironments - .filter((continuousEnvironment) => continuousEnvironment.isComplete) - .map( - (continuousEnvironment) => continuousEnvironment.startUpdateIndex, - ); - - this.simulationService.getMissingSimulationStates( - simulation.id, - wantedVisualizationTime, - completeStateUpdateIndexes, - ); - } - - const environmentSlice = this.environmentSignal(); - - this.timerService.isLoading = environmentSlice === null; - }); - - effect(() => { - const simulation = this.simulationService.activeSimulationSignal(); - - if (simulation === null) { - return; - } - - const polylines = this.simulationService.simulationPolylinesSignal(); - const isFetching = this.simulationService.isFetchingPolylinesSignal(); - - const needPolylineUpdate = - polylines === null || polylines.version !== simulation.polylinesVersion; + effect( + () => + (this.simulationService.wantedVisualizationTime = + this.wantedVisualizationTimeSignal()), + ); - if (needPolylineUpdate && !isFetching) { - this.simulationService.getPolylines(simulation.id); - } - }); + effect( + () => (this.timerService.isLoading = this.sliceEnvironment() === null), + ); // MARK: Animation effect(() => { @@ -299,6 +241,8 @@ export class VisualizationService { this.hasEnvironmentChanged = false; + this.environmentSlice = environment; + return environment; } } diff --git a/multimodal-ui/src/environments/environment.test.ts b/multimodal-ui/src/environments/environment.test.ts new file mode 100644 index 00000000..d1c4be4e --- /dev/null +++ b/multimodal-ui/src/environments/environment.test.ts @@ -0,0 +1,8 @@ +export const SIMULATION_SAVE_FILE_SEPARATOR = '---'; +export const DEBUG_TASKS = false; +export const MAX_STATES_EXTRACTION_CONCURRENT_TASKS = 10; + +export const environment = { + socketUrl: '', + apiUrl: '', +}; diff --git a/multimodal-ui/src/environments/environment.ts b/multimodal-ui/src/environments/environment.ts index 11db0c4c..85cf9459 100644 --- a/multimodal-ui/src/environments/environment.ts +++ b/multimodal-ui/src/environments/environment.ts @@ -5,6 +5,8 @@ const jsonEnvironment = ( SERVER_PORT: string; HOST: string; SIMULATION_SAVE_FILE_SEPARATOR: string; + DEBUG_TASKS: string; + MAX_STATES_EXTRACTION_CONCURRENT_TASKS: string; }; } ).environment; @@ -13,6 +15,10 @@ const SERVER_PORT = jsonEnvironment.SERVER_PORT; const HOST = jsonEnvironment.HOST; export const SIMULATION_SAVE_FILE_SEPARATOR = jsonEnvironment.SIMULATION_SAVE_FILE_SEPARATOR; +export const DEBUG_TASKS = jsonEnvironment.DEBUG_TASKS === 'true'; +export const MAX_STATES_EXTRACTION_CONCURRENT_TASKS = Number( + jsonEnvironment.MAX_STATES_EXTRACTION_CONCURRENT_TASKS, +); export const environment = { socketUrl: `:${SERVER_PORT}/`, diff --git a/multimodal-ui/tsconfig.app.json b/multimodal-ui/tsconfig.app.json index 8886e903..a0dcc37c 100644 --- a/multimodal-ui/tsconfig.app.json +++ b/multimodal-ui/tsconfig.app.json @@ -6,6 +6,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": ["src/main.ts"], - "include": ["src/**/*.d.ts"] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/multimodal-ui/tsconfig.json b/multimodal-ui/tsconfig.json index ab3965ce..081ec4b6 100644 --- a/multimodal-ui/tsconfig.json +++ b/multimodal-ui/tsconfig.json @@ -28,5 +28,14 @@ }, "ts-node": { "compilerOptions": { "module": "commonjs" } - } + }, + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] } diff --git a/multimodal-ui/tsconfig.spec.json b/multimodal-ui/tsconfig.spec.json index e00e30e6..e9773211 100644 --- a/multimodal-ui/tsconfig.spec.json +++ b/multimodal-ui/tsconfig.spec.json @@ -6,5 +6,5 @@ "outDir": "./out-tsc/spec", "types": ["jasmine"] }, - "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] + "include": ["src/**/*.ts"] } diff --git a/python/multimodalsim_viewer/common/environments/.env b/python/multimodalsim_viewer/common/environments/.env index 78f4c7b3..da9bb1fb 100644 --- a/python/multimodalsim_viewer/common/environments/.env +++ b/python/multimodalsim_viewer/common/environments/.env @@ -5,3 +5,5 @@ INPUT_DATA_DIRECTORY_PATH=data OUTPUT_DATA_DIRECTORY_PATH=output NUMBER_OF_UPDATES_BETWEEN_STATES=1000 NUMBER_OF_STATES_TO_SEND_AT_ONCE=1 +MAX_STATES_EXTRACTION_CONCURRENT_TASKS=10 +DEBUG_TASKS=false \ No newline at end of file diff --git a/python/multimodalsim_viewer/server/data_manager.py b/python/multimodalsim_viewer/server/data_manager.py index f02f18e7..70708b38 100644 --- a/python/multimodalsim_viewer/server/data_manager.py +++ b/python/multimodalsim_viewer/server/data_manager.py @@ -292,15 +292,15 @@ def get_missing_states( # pylint: disable=too-many-locals, too-many-branches, t visualization_time: float, complete_state_update_indexes: list[int], is_simulation_complete: bool, - ) -> tuple[list[dict], dict[list[str]], bool]: + ) -> tuple[list[dict], bool]: sorted_states = SimulationVisualizationDataManager.get_sorted_states(simulation_id) if len(sorted_states) == 0: - return [], {}, False + return [], False if len(complete_state_update_indexes) == len(sorted_states): # If the client has all states, no need to request more - return [], {}, True + return [], True necessary_state_index = None @@ -321,7 +321,6 @@ def get_missing_states( # pylint: disable=too-many-locals, too-many-branches, t necessary_state_index = max(0, necessary_state_index) missing_states = [] - missing_updates = {} has_incomplete_states = False # We want to load the necessary state first, followed by @@ -361,20 +360,25 @@ def get_missing_states( # pylint: disable=too-many-locals, too-many-branches, t has_incomplete_states = True state["isComplete"] = is_complete - missing_states.append(state) - updates_data = file.readlines() current_state_updates = [] for update_data in updates_data: current_state_updates.append(update_data) - missing_updates[update_index] = current_state_updates + missing_states.append( + { + "startTimestamp": state_timestamp, + "startUpdateIndex": update_index, + "environment": state, + "updates": current_state_updates, + } + ) has_all_states = ( len(missing_states) + len(complete_state_update_indexes) == len(sorted_states) and not has_incomplete_states ) - return (missing_states, missing_updates, has_all_states) + return (missing_states, has_all_states) # MARK: +- Polylines diff --git a/python/multimodalsim_viewer/server/simulation_manager.py b/python/multimodalsim_viewer/server/simulation_manager.py index f9a0992e..ae570dcb 100644 --- a/python/multimodalsim_viewer/server/simulation_manager.py +++ b/python/multimodalsim_viewer/server/simulation_manager.py @@ -556,7 +556,7 @@ def emit_missing_simulation_states( ) -> None: try: - (missing_states, missing_updates, has_all_states) = SimulationVisualizationDataManager.get_missing_states( + (missing_states, has_all_states) = SimulationVisualizationDataManager.get_missing_states( simulation_id, visualization_time, complete_state_update_indexes, @@ -565,7 +565,7 @@ def emit_missing_simulation_states( self.socketio.emit( "missing-simulation-states", - (missing_states, missing_updates, has_all_states), + (missing_states, has_all_states), to=get_session_id(), ) diff --git a/python/multimodalsim_viewer/ui/static/environment.json b/python/multimodalsim_viewer/ui/static/environment.json index acc41469..b3d75a2b 100644 --- a/python/multimodalsim_viewer/ui/static/environment.json +++ b/python/multimodalsim_viewer/ui/static/environment.json @@ -1,7 +1,9 @@ { "CLIENT_PORT": "8085", + "DEBUG_TASKS": "false", "HOST": "127.0.0.1", "INPUT_DATA_DIRECTORY_PATH": "data", + "MAX_STATES_EXTRACTION_CONCURRENT_TASKS": "10", "NUMBER_OF_STATES_TO_SEND_AT_ONCE": "1", "NUMBER_OF_UPDATES_BETWEEN_STATES": "1000", "OUTPUT_DATA_DIRECTORY_PATH": "output",