From 322809aff8648157adc03673e0d9f8f3e16f4d21 Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Mon, 2 Dec 2024 10:37:43 +0100 Subject: [PATCH 1/2] Improve UI/Memory performance by adding caching --- proto/api.proto | 1 + ui/core/cache_handler.ts | 33 ++++ ui/core/components/detailed_results.tsx | 6 +- .../detailed_results/log_runner.tsx | 22 +-- .../metrics_table/metrics_table.tsx | 44 +++-- .../detailed_results/player_damage.tsx | 2 +- .../detailed_results/player_damage_taken.tsx | 2 +- ...source_metrics.ts => resource_metrics.tsx} | 13 +- .../detailed_results/result_component.ts | 33 ++-- .../detailed_results/results_filter.ts | 10 +- .../components/detailed_results/timeline.tsx | 162 ++++++++++++------ .../individual_sim_ui/apl_helpers.tsx | 54 ++++-- ui/core/components/raid_sim_action.tsx | 76 ++++++-- ui/core/components/results_viewer.tsx | 3 +- ui/core/constants/lang.ts | 4 +- ui/core/proto_utils/logs_parser.tsx | 20 ++- ui/core/proto_utils/sim_result.ts | 17 +- ui/core/sim.ts | 4 +- ui/core/utils.ts | 6 + ui/core/worker_pool.ts | 4 +- .../detailed_results/_timeline.scss | 43 ++++- 21 files changed, 396 insertions(+), 163 deletions(-) create mode 100644 ui/core/cache_handler.ts rename ui/core/components/detailed_results/{resource_metrics.ts => resource_metrics.tsx} (86%) diff --git a/proto/api.proto b/proto/api.proto index f8fab8555f..57ff4828fa 100644 --- a/proto/api.proto +++ b/proto/api.proto @@ -360,6 +360,7 @@ message ErrorOutcome { // RPC RaidSim message RaidSimRequest { + string request_id = 5; Raid raid = 1; Encounter encounter = 2; SimOptions sim_options = 3; diff --git a/ui/core/cache_handler.ts b/ui/core/cache_handler.ts new file mode 100644 index 0000000000..0cc6ccd326 --- /dev/null +++ b/ui/core/cache_handler.ts @@ -0,0 +1,33 @@ +export type CacheHandlerOptions = { + keysToKeep?: number; +}; + +export class CacheHandler { + keysToKeep: CacheHandlerOptions['keysToKeep']; + private data = new Map(); + + constructor(options: CacheHandlerOptions = {}) { + this.keysToKeep = options.keysToKeep; + } + + has(id: string): boolean { + return this.data.has(id); + } + + get(id: string): T | undefined { + return this.data.get(id); + } + + set(id: string, result: T) { + this.data.set(id, result); + if (this.keysToKeep) this.keepMostRecent(); + } + + private keepMostRecent() { + if (this.data.size > 2) { + const keys = [...this.data.keys()]; + const keysToRemove = keys.slice(0, keys.length - 2); + keysToRemove.forEach(key => this.data.delete(key)); + } + } +} diff --git a/ui/core/components/detailed_results.tsx b/ui/core/components/detailed_results.tsx index a26f38e6a3..a811f9d076 100644 --- a/ui/core/components/detailed_results.tsx +++ b/ui/core/components/detailed_results.tsx @@ -408,8 +408,7 @@ export class EmbeddedDetailedResults extends DetailedResults { this.tabWindow = window.open(url.href, 'Detailed Results'); this.tabWindow!.addEventListener('load', async () => { if (this.latestRun) { - await this.updateSettings(); - await this.setSimRunData(this.latestRun); + await Promise.all([this.updateSettings(), this.setSimRunData(this.latestRun)]); } }); } else { @@ -425,8 +424,7 @@ export class EmbeddedDetailedResults extends DetailedResults { simResultsManager.currentChangeEmitter.on(async () => { const runData = simResultsManager.getRunData(); if (runData) { - await this.updateSettings(); - await this.setSimRunData(runData); + await Promise.all([this.updateSettings(), this.setSimRunData(runData)]); } }); } diff --git a/ui/core/components/detailed_results/log_runner.tsx b/ui/core/components/detailed_results/log_runner.tsx index 7ea45b3eee..34d6bc67fb 100644 --- a/ui/core/components/detailed_results/log_runner.tsx +++ b/ui/core/components/detailed_results/log_runner.tsx @@ -4,6 +4,7 @@ import { ref } from 'tsx-vanilla'; import { SimLog } from '../../proto_utils/logs_parser'; import { TypedEvent } from '../../typed_event.js'; +import { fragmentToString } from '../../utils'; import { BooleanPicker } from '../pickers/boolean_picker.js'; import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; export class LogRunner extends ResultComponent { @@ -18,7 +19,7 @@ export class LogRunner extends ResultComponent { contentContainer: HTMLTableSectionElement; }; cacheOutput: { - cacheKey: number | null; + cacheKey: string | null; logs: SimLog[] | null; logsAsHTML: Element[] | null; logsAsText: string[] | null; @@ -132,41 +133,34 @@ export class LogRunner extends ResultComponent { } onSimResult(resultData: SimResultData): void { - this.getLogs(resultData) - this.searchLogs(this.ui.search.value) + this.getLogs(resultData); + this.searchLogs(this.ui.search.value); } getLogs(resultData: SimResultData) { if (!resultData) return []; - if (this.cacheOutput.cacheKey === resultData?.eventID) { + const cacheKey = resultData.result.request.requestId; + if (this.cacheOutput.cacheKey === cacheKey) { return this.cacheOutput.logsAsHTML; } const validLogs = resultData.result.logs.filter(log => !log.isCastCompleted()); - this.cacheOutput.cacheKey = resultData?.eventID; + this.cacheOutput.cacheKey = cacheKey; this.cacheOutput.logs = validLogs; this.cacheOutput.logsAsHTML = validLogs.map(log => this.renderItem(log)); this.cacheOutput.logsAsText = this.cacheOutput.logsAsHTML.map(element => fragmentToString(element).trim().toLowerCase()); - - return this.cacheOutput.logsAsHTML; } renderItem(log: SimLog) { return ( {log.formattedTimestamp()} - {log.toHTML(false)} + {log.toHTML(false).cloneNode(true)} ) as HTMLTableRowElement; } } -const fragmentToString = (element: Node | Element) => { - const div = document.createElement('div'); - div.appendChild(element.cloneNode(true)); - return div.innerHTML; -}; - class CustomVirtualScroll { private scrollContainer: HTMLElement; private contentContainer: HTMLElement; diff --git a/ui/core/components/detailed_results/metrics_table/metrics_table.tsx b/ui/core/components/detailed_results/metrics_table/metrics_table.tsx index 2bb6c11f54..83d4232c21 100644 --- a/ui/core/components/detailed_results/metrics_table/metrics_table.tsx +++ b/ui/core/components/detailed_results/metrics_table/metrics_table.tsx @@ -1,6 +1,7 @@ import tippy from 'tippy.js'; import { ref } from 'tsx-vanilla'; +import { CacheHandler } from '../../../cache_handler'; import { TOOLTIP_METRIC_LABELS } from '../../../constants/tooltips'; import { ActionId } from '../../../proto_utils/action_id'; import { ActionMetrics, AuraMetrics, ResourceMetrics, UnitMetrics } from '../../../proto_utils/sim_result'; @@ -29,6 +30,8 @@ export interface MetricsColumnConfig { fillCell?: (metric: T, cellElem: HTMLElement, rowElem: HTMLElement, isChildRow?: boolean) => void; } +const cachedMetricsTableIcon = new CacheHandler(); + export abstract class MetricsTable extends ResultComponent { private readonly columnConfigs: Array>; @@ -50,13 +53,13 @@ export abstract class MetricsTable, ); - this.tableElem = this.rootElem.getElementsByClassName('metrics-table')[0] as HTMLTableSectionElement; - this.bodyElem = this.rootElem.getElementsByClassName('metrics-table-body')[0] as HTMLElement; + this.tableElem = this.rootElem.querySelector('.metrics-table')!; + this.bodyElem = this.rootElem.querySelector('.metrics-table-body')!; - const headerRowElem = this.rootElem.getElementsByClassName('metrics-table-header-row')[0] as HTMLElement; + const headerRowElem = this.rootElem.querySelector('.metrics-table-header-row')!; this.columnConfigs.forEach(columnConfig => { const headerCell = document.createElement('th'); - const tooltip = columnConfig.tooltip || TOOLTIP_METRIC_LABELS[columnConfig.name as keyof typeof TOOLTIP_METRIC_LABELS]; + const tooltipContent = columnConfig.tooltip || TOOLTIP_METRIC_LABELS[columnConfig.name as keyof typeof TOOLTIP_METRIC_LABELS]; headerCell.classList.add('metrics-table-header-cell'); if (columnConfig.columnClass) { headerCell.classList.add(...columnConfig.columnClass.split(' ')); @@ -65,11 +68,12 @@ export abstract class MetricsTable{columnConfig.name}); - if (tooltip) { - tippy(headerCell, { - content: tooltip, + if (tooltipContent) { + const tooltip = tippy(headerCell, { + content: tooltipContent, ignoreAttributes: true, }); + this.addOnResetCallback(() => tooltip.destroy()); } headerRowElem.appendChild(headerCell); }); @@ -158,14 +162,14 @@ export abstract class MetricsTable group.length > 0); - if (groupedMetrics.length == 0) { + if (groupedMetrics.length) { + this.rootElem.classList.remove('hide'); + } else { this.rootElem.classList.add('hide'); this.onUpdate.emit(resultData.eventID); return; - } else { - this.rootElem.classList.remove('hide'); } groupedMetrics.forEach(group => this.addGroup(group)); @@ -173,6 +177,11 @@ export abstract class MetricsTable { const data = getData(metric); - const iconElem = ref(); + const actionIdAsString = data.actionId.toString(); + const iconElemRef = ref(); + const iconElem = cachedMetricsTableIcon.get(actionIdAsString); cellElem.appendChild(
- + {iconElem?.cloneNode() || } {data.name}
, ); - if (iconElem.value) { - data.actionId.setBackgroundAndHref(iconElem.value); - data.actionId.setWowheadDataset(iconElem.value, { + if (!iconElem && iconElemRef.value) { + data.actionId.setBackgroundAndHref(iconElemRef.value); + data.actionId.setWowheadDataset(iconElemRef.value, { useBuffAura: data.metricType === 'AuraMetrics', }); + cachedMetricsTableIcon.set(actionIdAsString, iconElemRef.value); } }, }; diff --git a/ui/core/components/detailed_results/player_damage.tsx b/ui/core/components/detailed_results/player_damage.tsx index f2f7bb3f94..04b9b132ea 100644 --- a/ui/core/components/detailed_results/player_damage.tsx +++ b/ui/core/components/detailed_results/player_damage.tsx @@ -88,7 +88,7 @@ export class PlayerDamageMetricsTable extends MetricsTable { customizeRowElem(player: UnitMetrics, rowElem: HTMLElement) { rowElem.classList.add('player-damage-row'); - rowElem.addEventListener('click', event => { + rowElem.addEventListener('click', () => { this.resultsFilter.setPlayer(this.getLastSimResult().eventID, player.index); }); } diff --git a/ui/core/components/detailed_results/player_damage_taken.tsx b/ui/core/components/detailed_results/player_damage_taken.tsx index 845f1cc5f8..bbe5d3117f 100644 --- a/ui/core/components/detailed_results/player_damage_taken.tsx +++ b/ui/core/components/detailed_results/player_damage_taken.tsx @@ -90,7 +90,7 @@ export class PlayerDamageTakenMetricsTable extends MetricsTable { customizeRowElem(player: UnitMetrics, rowElem: HTMLElement) { rowElem.classList.add('player-damage-row'); - rowElem.addEventListener('click', event => { + rowElem.addEventListener('click', () => { this.resultsFilter.setPlayer(this.getLastSimResult().eventID, player.index); }); } diff --git a/ui/core/components/detailed_results/resource_metrics.ts b/ui/core/components/detailed_results/resource_metrics.tsx similarity index 86% rename from ui/core/components/detailed_results/resource_metrics.ts rename to ui/core/components/detailed_results/resource_metrics.tsx index adaded2d45..bd4f0fe557 100644 --- a/ui/core/components/detailed_results/resource_metrics.ts +++ b/ui/core/components/detailed_results/resource_metrics.tsx @@ -1,4 +1,3 @@ -import { TOOLTIP_METRIC_LABELS } from '../../constants/tooltips'; import { ResourceType } from '../../proto/api'; import { resourceNames } from '../../proto_utils/names'; import { ResourceMetrics } from '../../proto_utils/sim_result'; @@ -12,14 +11,14 @@ export class ResourceMetricsTable extends ResultComponent { super(config); orderedResourceTypes.forEach(resourceType => { - const containerElem = document.createElement('div'); - containerElem.classList.add('resource-metrics-table-container', 'hide'); - containerElem.innerHTML = `${resourceNames.get(resourceType)}`; + const containerElem = ( +
+ {resourceNames.get(resourceType)} +
+ ) as HTMLElement; this.rootElem.appendChild(containerElem); - const childConfig = config; - childConfig.parent = containerElem; - const table = new TypedResourceMetricsTable(childConfig, resourceType); + const table = new TypedResourceMetricsTable({ ...config, parent: containerElem }, resourceType); table.onUpdate.on(() => { if (table.rootElem.classList.contains('hide')) { containerElem.classList.add('hide'); diff --git a/ui/core/components/detailed_results/result_component.ts b/ui/core/components/detailed_results/result_component.ts index 48aa78aec5..630b6bee2e 100644 --- a/ui/core/components/detailed_results/result_component.ts +++ b/ui/core/components/detailed_results/result_component.ts @@ -3,28 +3,28 @@ import { SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; import { EventID, TypedEvent } from '../../typed_event.js'; export interface SimResultData { - eventID: EventID, - result: SimResult, - filter: SimResultFilter, -}; + eventID: EventID; + result: SimResult; + filter: SimResultFilter; +} export interface ResultComponentConfig { - parent: HTMLElement, - rootCssClass?: string, - cssScheme?: string | null, - resultsEmitter: TypedEvent, -}; + parent: HTMLElement; + rootCssClass?: string; + cssScheme?: string | null; + resultsEmitter: TypedEvent; +} export abstract class ResultComponent extends Component { lastSimResult: SimResultData | null; + private resetCallbacks: (() => void)[] = []; constructor(config: ResultComponentConfig) { super(config.parent, config.rootCssClass || 'result-component'); this.lastSimResult = null; config.resultsEmitter.on((_, resultData) => { - if (!resultData) - return; + if (!resultData) return; this.lastSimResult = resultData; this.onSimResult(resultData); @@ -32,7 +32,7 @@ export abstract class ResultComponent extends Component { } hasLastSimResult(): boolean { - return this.lastSimResult != null; + return !!this.lastSimResult; } getLastSimResult(): SimResultData { @@ -44,4 +44,13 @@ export abstract class ResultComponent extends Component { } abstract onSimResult(resultData: SimResultData): void; + + addOnResetCallback(callback: () => void) { + this.resetCallbacks.push(callback); + } + + reset() { + this.resetCallbacks.forEach(callback => callback()); + this.resetCallbacks = []; + } } diff --git a/ui/core/components/detailed_results/results_filter.ts b/ui/core/components/detailed_results/results_filter.ts index ac99adf6d1..d58f78bfc9 100644 --- a/ui/core/components/detailed_results/results_filter.ts +++ b/ui/core/components/detailed_results/results_filter.ts @@ -1,8 +1,8 @@ -import { UnitPicker, UnitValue, UnitValueConfig } from '../pickers/unit_picker.jsx'; -import { UnitReference, UnitReference_Type as UnitType } from '../../proto/common.js'; -import { SimResult, SimResultFilter } from '../../proto_utils/sim_result.js'; -import { EventID, TypedEvent } from '../../typed_event.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { UnitReference, UnitReference_Type as UnitType } from '../../proto/common'; +import { SimResult, SimResultFilter } from '../../proto_utils/sim_result'; +import { EventID, TypedEvent } from '../../typed_event'; +import { UnitPicker, UnitValue, UnitValueConfig } from '../pickers/unit_picker'; +import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component'; const ALL_UNITS = -1; diff --git a/ui/core/components/detailed_results/timeline.tsx b/ui/core/components/detailed_results/timeline.tsx index 641a73f871..e1f7401863 100644 --- a/ui/core/components/detailed_results/timeline.tsx +++ b/ui/core/components/detailed_results/timeline.tsx @@ -1,17 +1,18 @@ import tippy from 'tippy.js'; import { ref } from 'tsx-vanilla'; -import { ResourceType } from '../../proto/api.js'; -import { OtherAction } from '../../proto/common.js'; -import { ActionId, buffAuraToSpellIdMap, resourceTypeToIcon } from '../../proto_utils/action_id.js'; -import { AuraUptimeLog, CastLog, DpsLog, ResourceChangedLogGroup, SimLog, ThreatLogGroup } from '../../proto_utils/logs_parser.js'; -import { resourceNames } from '../../proto_utils/names.js'; -import { UnitMetrics } from '../../proto_utils/sim_result.js'; -import { orderedResourceTypes } from '../../proto_utils/utils.js'; -import { TypedEvent } from '../../typed_event.js'; -import { bucket, distinct, maxIndex, stringComparator } from '../../utils.js'; -import { actionColors } from './color_settings.js'; -import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component.js'; +import { CacheHandler } from '../../cache_handler'; +import { ResourceType } from '../../proto/api'; +import { OtherAction } from '../../proto/common'; +import { ActionId, buffAuraToSpellIdMap, resourceTypeToIcon } from '../../proto_utils/action_id'; +import { AuraUptimeLog, CastLog, DpsLog, ResourceChangedLogGroup, SimLog, ThreatLogGroup } from '../../proto_utils/logs_parser'; +import { resourceNames } from '../../proto_utils/names'; +import { UnitMetrics } from '../../proto_utils/sim_result'; +import { orderedResourceTypes } from '../../proto_utils/utils'; +import { TypedEvent } from '../../typed_event'; +import { bucket, distinct, fragmentToString, maxIndex, stringComparator } from '../../utils'; +import { actionColors } from './color_settings'; +import { ResultComponent, ResultComponentConfig, SimResultData } from './result_component'; declare let ApexCharts: any; @@ -21,6 +22,8 @@ const dpsColor = '#ed5653'; const manaColor = '#2E93fA'; const threatColor = '#b56d07'; +const cachedSpellCastIcon = new CacheHandler(); + export class Timeline extends ResultComponent { private readonly dpsResourcesPlotElem: HTMLElement; private dpsResourcesPlot: any; @@ -28,21 +31,32 @@ export class Timeline extends ResultComponent { private readonly rotationPlotElem: HTMLElement; private readonly rotationLabels: HTMLElement; private readonly rotationTimeline: HTMLElement; + private rotationTimelineTimeRulerElem: HTMLCanvasElement | null = null; private readonly rotationHiddenIdsContainer: HTMLElement; private readonly chartPicker: HTMLSelectElement; + private prevResultData: SimResultData | null; private resultData: SimResultData | null; - private rendered: boolean; + rendered: boolean; private hiddenIds: Array; private hiddenIdsChangeEmitter; - - private resetCallbacks: (() => void)[] = []; + private cacheHandler = new CacheHandler<{ + dpsResourcesPlotOptions: any; + rotationLabels: Timeline['rotationLabels']; + rotationTimeline: Timeline['rotationTimeline']; + rotationHiddenIdsContainer: Timeline['rotationHiddenIdsContainer']; + rotationTimelineTimeRulerElem: Timeline['rotationTimelineTimeRulerElem']; + rotationTimelineTimeRulerImage: ImageData | undefined; + }>({ + keysToKeep: 2, + }); constructor(config: ResultComponentConfig) { config.rootCssClass = 'timeline-root'; super(config); this.resultData = null; + this.prevResultData = null; this.rendered = false; this.hiddenIds = []; this.hiddenIdsChangeEmitter = new TypedEvent(); @@ -86,16 +100,7 @@ export class Timeline extends ResultComponent { ); this.chartPicker = this.rootElem.querySelector('.timeline-chart-picker')!; - this.chartPicker.addEventListener('change', () => { - if (this.chartPicker.value == 'rotation') { - this.dpsResourcesPlotElem.classList.add('hide'); - this.rotationPlotElem.classList.remove('hide'); - } else { - this.dpsResourcesPlotElem.classList.remove('hide'); - this.rotationPlotElem.classList.add('hide'); - } - this.updatePlot(); - }); + this.chartPicker.addEventListener('change', () => this.onChartPickerSelectHandler()); this.dpsResourcesPlotElem = this.rootElem.querySelector('.dps-resources-plot')!; this.dpsResourcesPlot = new ApexCharts(this.dpsResourcesPlotElem, { @@ -155,12 +160,20 @@ export class Timeline extends ResultComponent { }); } + onChartPickerSelectHandler() { + if (this.chartPicker.value === 'rotation') { + this.dpsResourcesPlotElem.classList.add('hide'); + this.rotationPlotElem.classList.remove('hide'); + } else { + this.dpsResourcesPlotElem.classList.remove('hide'); + this.rotationPlotElem.classList.add('hide'); + } + } + onSimResult(resultData: SimResultData) { + this.prevResultData = this.resultData; this.resultData = resultData; - - if (this.rendered) { - this.updatePlot(); - } + this.update(); } private updatePlot() { @@ -168,8 +181,29 @@ export class Timeline extends ResultComponent { return; } + const cachedData = this.cacheHandler.get(this.resultData.result.request.requestId); + if (cachedData) { + const { dpsResourcesPlotOptions, rotationLabels, rotationTimeline, rotationHiddenIdsContainer, rotationTimelineTimeRulerImage } = cachedData; + this.rotationLabels.replaceChildren(...rotationLabels.cloneNode(true).childNodes); + this.rotationTimeline.replaceChildren(...rotationTimeline.cloneNode(true).childNodes); + this.rotationHiddenIdsContainer.replaceChildren(...rotationHiddenIdsContainer.cloneNode(true).childNodes); + this.dpsResourcesPlot.updateOptions(dpsResourcesPlotOptions); + + if (rotationTimelineTimeRulerImage) + this.rotationTimeline + .querySelector('.rotation-timeline-canvas') + ?.getContext('2d') + ?.putImageData(rotationTimelineTimeRulerImage, 0, 0); + + this.onChartPickerSelectHandler(); + return; + } + const duration = this.resultData!.result.result.firstIterationDuration || 1; const options: any = { + theme: { + mode: 'dark', + }, series: [], colors: [], xaxis: { @@ -207,7 +241,7 @@ export class Timeline extends ResultComponent { enabled: true, custom: (data: { series: any; seriesIndex: number; dataPointIndex: number; w: any }) => { if (tooltipHandlers[data.seriesIndex]) { - return tooltipHandlers[data.seriesIndex]!(data.dataPointIndex); + return fragmentToString(tooltipHandlers[data.seriesIndex]!(data.dataPointIndex)); } else { throw new Error('No tooltip handler for series ' + data.seriesIndex); } @@ -234,7 +268,7 @@ export class Timeline extends ResultComponent { tooltipHandlers.push(dpsData.tooltipHandler); tooltipHandlers.push(this.addManaSeries(player, options)); tooltipHandlers.push(this.addThreatSeries(player, options, '')); - tooltipHandlers = tooltipHandlers.filter(handler => handler != null); + tooltipHandlers = tooltipHandlers.filter(handler => !!handler); this.addMajorCooldownAnnotations(player, options); } else { @@ -269,6 +303,19 @@ export class Timeline extends ResultComponent { } this.dpsResourcesPlot.updateOptions(options); + + this.rotationTimelineTimeRulerElem?.toBlob(blob => { + this.cacheHandler.set(this.resultData!.result.request.requestId, { + dpsResourcesPlotOptions: options, + rotationLabels: this.rotationLabels.cloneNode(true) as HTMLElement, + rotationTimeline: this.rotationTimeline.cloneNode(true) as HTMLElement, + rotationHiddenIdsContainer: this.rotationHiddenIdsContainer.cloneNode(true) as HTMLElement, + rotationTimelineTimeRulerElem: this.rotationTimelineTimeRulerElem?.cloneNode(true) as HTMLCanvasElement, + rotationTimelineTimeRulerImage: this.rotationTimelineTimeRulerElem + ?.getContext('2d') + ?.getImageData(0, 0, this.rotationTimelineTimeRulerElem.width, this.rotationTimelineTimeRulerElem.height), + }); + }); } private addDpsYAxis(maxDps: number, options: any) { @@ -479,16 +526,15 @@ export class Timeline extends ResultComponent { } private clearRotationChart() { - this.rotationLabels.innerText = ''; - this.rotationLabels.appendChild(
); - - this.rotationTimeline.innerText = ''; - this.rotationTimeline.appendChild( + this.rotationLabels.replaceChildren(
); + const canvasRef = ref(); + this.rotationTimeline.replaceChildren(
- +
, ); - this.rotationHiddenIdsContainer.innerText = ''; + this.rotationTimelineTimeRulerElem = canvasRef.value || null; + this.rotationHiddenIdsContainer.replaceChildren(); this.hiddenIdsChangeEmitter = new TypedEvent(); } @@ -825,8 +871,14 @@ export class Timeline extends ResultComponent { } } - const iconElem = () as HTMLAnchorElement; - actionId.setBackground(iconElem); + const actionIdAsString = actionId.toString(); + const cachedIconElem = cachedSpellCastIcon.get(actionIdAsString)?.cloneNode() as HTMLAnchorElement | undefined; + let iconElem = cachedIconElem; + if (!iconElem) { + iconElem = () as HTMLAnchorElement; + actionId.setBackground(iconElem); + cachedSpellCastIcon.set(actionIdAsString, iconElem); + } castElem.appendChild(iconElem); const travelTimeStr = castLog.travelTime == 0 ? '' : ` + ${castLog.travelTime.toFixed(2)}s travel time`; @@ -859,10 +911,11 @@ export class Timeline extends ResultComponent { ); - tippy(castElem, { + const tooltip = tippy(castElem, { placement: 'bottom', content: tt, }); + this.addOnResetCallback(() => tooltip.destroy()); castLog.damageDealtLogs .filter(ddl => ddl.tick) @@ -886,10 +939,11 @@ export class Timeline extends ResultComponent { ); - tippy(tickElem, { + const tooltip = tippy(tickElem, { placement: 'bottom', content: tt, }); + this.addOnResetCallback(() => tooltip.destroy()); }); }); @@ -1030,7 +1084,7 @@ export class Timeline extends ResultComponent { {log.timestamp.toFixed(2)}s
-
    {log.damageLogs.map(damageLog => this.tooltipLogItem(damageLog, damageLog.result())).join('')}
+
    {log.damageLogs.map(damageLog => this.tooltipLogItem(damageLog, damageLog.result()))}
DPS: {log.dps.toFixed(2)}
@@ -1060,7 +1114,7 @@ export class Timeline extends ResultComponent {
Before: {log.threatBefore.toFixed(1)}
-
    {log.logs.map(log => this.tooltipLogItem(log, <>{log.threat.toFixed(1)} Threat)).join('')}
+
    {log.logs.map(log => this.tooltipLogItem(log, <>{log.threat.toFixed(1)} Threat))}
After: {log.threatAfter.toFixed(1)}
@@ -1143,20 +1197,22 @@ export class Timeline extends ResultComponent { ); } - render() { + update() { this.reset(); - this.dpsResourcesPlot.render(); - this.rendered = true; + if (!this.rendered) this.dpsResourcesPlot.render(); this.updatePlot(); + this.rendered = true; } - addOnResetCallback(callback: () => void) { - this.resetCallbacks.push(callback); + render() { + if (this.rendered) return; + this.update(); } reset() { - this.resetCallbacks.forEach(callback => callback()); - this.resetCallbacks = []; + const previousResultRequestId = this.prevResultData?.result.request.requestId; + if (previousResultRequestId && !this.cacheHandler.get(previousResultRequestId)) return; + super.reset(); } } @@ -1388,16 +1444,16 @@ const idToCategoryMap: Record = { [40536]: SPELL_ACTION_CATEGORY + 0.942, // Explosive Decoy [41119]: SPELL_ACTION_CATEGORY + 0.943, // Saronite Bomb [40771]: SPELL_ACTION_CATEGORY + 0.944, // Cobalt Frag Bomb - + // Souldrinker - to pair up the damage part with the healing [109828]: SPELL_ACTION_CATEGORY + 0.945, // Drain Life - LFR [108022]: SPELL_ACTION_CATEGORY + 0.946, // Drain Life - Normal [109831]: SPELL_ACTION_CATEGORY + 0.947, // Drain Life - Heroic - + // No'Kaled - to pair up the different spells it can proc [109871]: SPELL_ACTION_CATEGORY + 0.948, // Flameblast - LFR [109869]: SPELL_ACTION_CATEGORY + 0.949, // Iceblast - LFR - [109867]: SPELL_ACTION_CATEGORY + 0.950, // Shadowblast - LFR + [109867]: SPELL_ACTION_CATEGORY + 0.95, // Shadowblast - LFR [107785]: SPELL_ACTION_CATEGORY + 0.951, // Flameblast - Normal [107789]: SPELL_ACTION_CATEGORY + 0.952, // Iceblast - Normal [107787]: SPELL_ACTION_CATEGORY + 0.953, // Shadowblast - Normal diff --git a/ui/core/components/individual_sim_ui/apl_helpers.tsx b/ui/core/components/individual_sim_ui/apl_helpers.tsx index 21f41a2ede..b3ae1d2579 100644 --- a/ui/core/components/individual_sim_ui/apl_helpers.tsx +++ b/ui/core/components/individual_sim_ui/apl_helpers.tsx @@ -1,5 +1,6 @@ import { ref } from 'tsx-vanilla'; +import { CacheHandler } from '../../cache_handler'; import { Player, UnitMetadata } from '../../player.js'; import { APLValueEclipsePhase, APLValueRuneSlot, APLValueRuneType } from '../../proto/apl.js'; import { ActionID, OtherAction, Stat, UnitReference, UnitReference_Type as UnitType } from '../../proto/common.js'; @@ -205,16 +206,18 @@ const actionIdSets: Record< dot_spells: { defaultLabel: 'DoT Spell', getActionIDs: async metadata => { - return metadata - .getSpells() - .filter(spell => spell.data.hasDot) - // filter duplicate dot entries from RelatedDotSpell - .filter((value, index, self) => self.findIndex(v => v.id.anyId() === value.id.anyId()) === index) - .map(actionId => { - return { - value: actionId.id, - }; - }); + return ( + metadata + .getSpells() + .filter(spell => spell.data.hasDot) + // filter duplicate dot entries from RelatedDotSpell + .filter((value, index, self) => self.findIndex(v => v.id.anyId() === value.id.anyId()) === index) + .map(actionId => { + return { + value: actionId.id, + }; + }) + ); }, }, castable_dot_spells: { @@ -256,6 +259,8 @@ export interface APLActionIDPickerConfig setValue: (eventID: EventID, obj: ModObject, newValue: ActionID) => void; } +const cachedAPLActionIDPickerContent = new CacheHandler(); + export class APLActionIDPicker extends DropdownPicker, ActionID, ActionId> { constructor(parent: HTMLElement, player: Player, config: APLActionIDPickerConfig>) { const actionIdSet = actionIdSets[config.actionIdSet]; @@ -267,9 +272,16 @@ export class APLActionIDPicker extends DropdownPicker, ActionID, Act equals: (a, b) => (a == null) == (b == null) && (!a || a.equals(b!)), setOptionContent: (button, valueConfig) => { const actionId = valueConfig.value; - const iconRef = ref(); const isAuraType = ['auras', 'stackable_auras', 'icd_auras', 'exclusive_effect_auras'].includes(config.actionIdSet); - button.appendChild( + + const cacheKey = `${actionId.toString()}${isAuraType}`; + const cachedContent = cachedAPLActionIDPickerContent.get(cacheKey)?.cloneNode(true) as Element | undefined; + if (cachedContent) { + button.appendChild(cachedContent); + } + + const iconRef = ref(); + const content = ( <>
, ActionID, Act }} /> {actionId.name} - , + ); + button.appendChild(content); actionId.setBackgroundAndHref(iconRef.value!); actionId.setWowheadDataset(iconRef.value!, { useBuffAura: isAuraType }); + + cachedAPLActionIDPickerContent.set(cacheKey, content); }, createMissingValue: value => { if (value.anyId() == 0) { @@ -764,9 +779,11 @@ export function rotationTypeFieldConfig(field: string): APLPickerBuilderFieldCon export function statTypeFieldConfig(field: string): APLPickerBuilderFieldConfig { const allStats = getEnumValues(Stat) as Array; - const values = [{ value: -1, label: 'None' }].concat(allStats.map(stat => { - return { value: stat, label: getStatName(stat) }; - })); + const values = [{ value: -1, label: 'None' }].concat( + allStats.map(stat => { + return { value: stat, label: getStatName(stat) }; + }), + ); return { field: field, @@ -785,8 +802,9 @@ export function statTypeFieldConfig(field: string): APLPickerBuilderFieldConfig< export const minIcdInput = numberFieldConfig('minIcdSeconds', false, { label: 'Min ICD', - labelTooltip: 'If non-zero, filter out any procs that either lack an ICD or for which the ICD is smaller than the specified value (in seconds). This can be useful for certain snapshotting checks, since procs with low ICDs are often too weak to snapshot.', -}) + labelTooltip: + 'If non-zero, filter out any procs that either lack an ICD or for which the ICD is smaller than the specified value (in seconds). This can be useful for certain snapshotting checks, since procs with low ICDs are often too weak to snapshot.', +}); export function aplInputBuilder( newValue: () => T, diff --git a/ui/core/components/raid_sim_action.tsx b/ui/core/components/raid_sim_action.tsx index fca0a7dcdc..f8de6b1f83 100644 --- a/ui/core/components/raid_sim_action.tsx +++ b/ui/core/components/raid_sim_action.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; import tippy from 'tippy.js'; -import { ref } from 'tsx-vanilla'; import { TOOLTIP_METRIC_LABELS } from '../constants/tooltips'; import { DistributionMetrics as DistributionMetricsProto, ProgressMetrics, Raid as RaidProto } from '../proto/api'; @@ -125,6 +124,8 @@ export class RaidSimResultsManager { private currentData: ReferenceData | null = null; private referenceData: ReferenceData | null = null; + private resetCallbacks: (() => void)[] = []; + constructor(simUI: SimUI) { this.simUI = simUI; @@ -165,6 +166,7 @@ export class RaidSimResultsManager { raidProto: RaidProto.clone(simResult.request.raid || RaidProto.create()), encounterProto: EncounterProto.clone(simResult.request.encounter || EncounterProto.create()), }; + this.currentChangeEmitter.emit(eventID); this.simUI.resultsViewer.setContent( @@ -192,7 +194,8 @@ export class RaidSimResultsManager { const setResultTooltip = (selector: string, content: Element | HTMLElement | string) => { const resultDivElem = this.simUI.resultsViewer.contentElem.querySelector(selector); if (resultDivElem) { - tippy(resultDivElem, { content, placement: 'right' }); + const tooltip = tippy(resultDivElem, { content, placement: 'right' }); + this.addOnResetCallback(() => tooltip.destroy()); } }; setResultTooltip(`.${RaidSimResultsManager.resultMetricClasses['dps']}`, 'Damage Per Second'); @@ -237,17 +240,22 @@ export class RaidSimResultsManager { const simReferenceSetButton = this.simUI.resultsViewer.contentElem.querySelector('.results-sim-set-reference'); if (simReferenceSetButton) { - simReferenceSetButton.addEventListener('click', () => { + const onSetReferenceClickHandler = () => { this.referenceData = this.currentData; this.referenceChangeEmitter.emit(TypedEvent.nextEventID()); this.updateReference(); + }; + simReferenceSetButton.addEventListener('click', onSetReferenceClickHandler); + const tooltip = tippy(simReferenceSetButton, { content: 'Use as reference' }); + this.addOnResetCallback(() => { + tooltip.destroy(); + simReferenceSetButton?.removeEventListener('click', onSetReferenceClickHandler); }); - tippy(simReferenceSetButton, { content: 'Use as reference' }); } const simReferenceSwapButton = this.simUI.resultsViewer.contentElem.querySelector('.results-sim-reference-swap'); if (simReferenceSwapButton) { - simReferenceSwapButton.addEventListener('click', () => { + const onSwapClickHandler = () => { TypedEvent.freezeAllAndDo(() => { if (this.currentData && this.referenceData) { const swapEventID = TypedEvent.nextEventID(); @@ -263,23 +271,34 @@ export class RaidSimResultsManager { this.updateReference(); } }); - }); - tippy(simReferenceSwapButton, { + }; + simReferenceSwapButton.addEventListener('click', onSwapClickHandler); + const tooltip = tippy(simReferenceSwapButton, { content: 'Swap reference with current', ignoreAttributes: true, }); + this.addOnResetCallback(() => { + tooltip.destroy(); + simReferenceSwapButton?.removeEventListener('click', onSwapClickHandler); + }); } const simReferenceDeleteButton = this.simUI.resultsViewer.contentElem.querySelector('.results-sim-reference-delete'); if (simReferenceDeleteButton) { - simReferenceDeleteButton.addEventListener('click', () => { + const onDeleteReferenceClickHandler = () => { this.referenceData = null; this.referenceChangeEmitter.emit(TypedEvent.nextEventID()); this.updateReference(); - }); - tippy(simReferenceDeleteButton, { + }; + simReferenceDeleteButton.addEventListener('click', onDeleteReferenceClickHandler); + const tooltip = tippy(simReferenceDeleteButton, { content: 'Remove reference', ignoreAttributes: true, }); + + this.addOnResetCallback(() => { + tooltip.destroy(); + simReferenceDeleteButton?.removeEventListener('click', onDeleteReferenceClickHandler); + }); } this.updateReference(); @@ -349,12 +368,30 @@ export class RaidSimResultsManager { } else { const curMetrics = curMetricsTemp as DistributionMetricsProto; const refMetrics = refMetricsTemp as DistributionMetricsProto; - const isDiff = this.applyZTestTooltip(elem, ref.iterations, refMetrics.avg, refMetrics.stdev, cur.iterations, curMetrics.avg, curMetrics.stdev, !!preNormalizedErrors); + const isDiff = this.applyZTestTooltip( + elem, + ref.iterations, + refMetrics.avg, + refMetrics.stdev, + cur.iterations, + curMetrics.avg, + curMetrics.stdev, + !!preNormalizedErrors, + ); formatDeltaTextElem(elem, refMetrics.avg, curMetrics.avg, precision, lowerIsBetter, !isDiff); } } - private applyZTestTooltip(elem: HTMLElement, n1: number, avg1: number, stdev1: number, n2: number, avg2: number, stdev2: number, preNormalized: boolean): boolean { + private applyZTestTooltip( + elem: HTMLElement, + n1: number, + avg1: number, + stdev1: number, + n2: number, + avg2: number, + stdev2: number, + preNormalized: boolean, + ): boolean { const delta = avg1 - avg2; const err1 = preNormalized ? stdev1 : stdev1 / Math.sqrt(n1); const err2 = preNormalized ? stdev2 : stdev2 / Math.sqrt(n2); @@ -395,7 +432,7 @@ export class RaidSimResultsManager { // Defensive copy. return { simResult: this.currentData.simResult, - settings: JSON.parse(JSON.stringify(this.currentData.settings)), + settings: structuredClone(this.currentData.settings), raidProto: this.currentData.raidProto, encounterProto: this.currentData.encounterProto, }; @@ -409,7 +446,7 @@ export class RaidSimResultsManager { // Defensive copy. return { simResult: this.referenceData.simResult, - settings: JSON.parse(JSON.stringify(this.referenceData.settings)), + settings: structuredClone(this.referenceData.settings), raidProto: this.referenceData.raidProto, encounterProto: this.referenceData.encounterProto, }; @@ -656,7 +693,7 @@ export class RaidSimResultsManager { return ( <> {data.map(column => { - const errorDecimals = (column.unit === 'percentage') ? 2 : 0; + const errorDecimals = column.unit === 'percentage' ? 2 : 0; return (
{column.average.toFixed(2)} @@ -675,6 +712,15 @@ export class RaidSimResultsManager { ); } + + addOnResetCallback(callback: () => void) { + this.resetCallbacks.push(callback); + } + + reset() { + this.resetCallbacks.forEach(callback => callback()); + this.resetCallbacks = []; + } } type ToplineResultOptions = { diff --git a/ui/core/components/results_viewer.tsx b/ui/core/components/results_viewer.tsx index 38679e26d6..f39eaa542d 100644 --- a/ui/core/components/results_viewer.tsx +++ b/ui/core/components/results_viewer.tsx @@ -126,8 +126,7 @@ export class ResultsViewer extends Component { if (typeof html === 'string') { this.contentElem.innerHTML = html; } else { - this.contentElem.innerHTML = ''; - this.contentElem.appendChild(html); + this.contentElem.replaceChildren(html); } this.contentElem.style.display = 'block'; this.pendingElem.style.display = 'none'; diff --git a/ui/core/constants/lang.ts b/ui/core/constants/lang.ts index ee6d487c17..fd132d6580 100644 --- a/ui/core/constants/lang.ts +++ b/ui/core/constants/lang.ts @@ -34,5 +34,5 @@ export function setLanguageCode(newLang: string) { cachedWowheadLanguagePrefix_ = cachedLanguageCode_ ? cachedLanguageCode_ + '/' : ''; } -let cachedLanguageCode_: string = ''; -let cachedWowheadLanguagePrefix_: string = ''; +let cachedLanguageCode_ = ''; +let cachedWowheadLanguagePrefix_ = ''; diff --git a/ui/core/proto_utils/logs_parser.tsx b/ui/core/proto_utils/logs_parser.tsx index 92ec99f390..5ed12e70fc 100644 --- a/ui/core/proto_utils/logs_parser.tsx +++ b/ui/core/proto_utils/logs_parser.tsx @@ -1,5 +1,6 @@ import clsx from 'clsx'; +import { CacheHandler } from '../cache_handler'; import { RaidSimResult, ResourceType } from '../proto/api.js'; import { SpellSchool } from '../proto/common'; import { bucket, getEnumValues, stringComparator, sum } from '../utils.js'; @@ -87,6 +88,8 @@ interface SimLogParams { threat: number; } +const cachedActionIdLink = new CacheHandler(); + export class SimLog { readonly raw: string; @@ -101,6 +104,7 @@ export class SimLog { readonly source: Entity | null; readonly target: Entity | null; readonly actionId: ActionId | null; + readonly actionIdAsString: string | null; // Spell schoool from this event. Note that not all events have spell schools, so this will be 0null. readonly spellSchool: SpellSchool | null; @@ -120,6 +124,7 @@ export class SimLog { this.source = params.source; this.target = params.target; this.actionId = params.actionId; + this.actionIdAsString = this.actionId?.toString() || null; this.spellSchool = params.spellSchool; this.threat = params.threat; this.activeAuras = []; @@ -180,17 +185,22 @@ export class SimLog { } protected newActionIdLink(isAura?: boolean) { - const iconElem = ; + const cacheKey = this.actionIdAsString ? `${this.actionIdAsString}${isAura || ''}` : undefined; + const cachedLink = cacheKey ? cachedActionIdLink.get(cacheKey) : null; + if (cachedLink) return cachedLink.cloneNode(true); + + const iconElem = () as HTMLSpanElement; const actionAnchor = ( {iconElem} {this.actionId!.name} - ); - this.actionId?.setBackground(iconElem as HTMLAnchorElement); - this.actionId?.setWowheadHref(actionAnchor as HTMLAnchorElement); - this.actionId?.setWowheadDataset(actionAnchor as HTMLAnchorElement, { useBuffAura: isAura }); + ) as HTMLAnchorElement; + this.actionId?.setBackground(iconElem); + this.actionId?.setWowheadHref(actionAnchor); + this.actionId?.setWowheadDataset(actionAnchor, { useBuffAura: isAura }); + if (cacheKey) cachedActionIdLink.set(cacheKey, actionAnchor.cloneNode(true) as HTMLAnchorElement); return actionAnchor; } diff --git a/ui/core/proto_utils/sim_result.ts b/ui/core/proto_utils/sim_result.ts index a4fbada195..3a9e95f47d 100644 --- a/ui/core/proto_utils/sim_result.ts +++ b/ui/core/proto_utils/sim_result.ts @@ -1,3 +1,4 @@ +import { CacheHandler } from '../cache_handler'; import { PlayerSpec } from '../player_spec.js'; import { PlayerSpecs } from '../player_specs'; import { @@ -34,6 +35,10 @@ import { ThreatLogGroup, } from './logs_parser.js'; +const simResultsCache = new CacheHandler({ + keysToKeep: 2, +}); + export interface SimResultFilter { // Raid index of the player to display, or null for all players. player?: number | null; @@ -67,6 +72,7 @@ class SimResultData { // Holds all the data from a simulation call, and provides helper functions // for parsing it. export class SimResult { + readonly id: string; readonly request: RaidSimRequest; readonly result: RaidSimResult; @@ -78,6 +84,7 @@ export class SimResult { private units: Array; private constructor(request: RaidSimRequest, result: RaidSimResult, raidMetrics: RaidMetrics, encounterMetrics: EncounterMetrics, logs: Array) { + this.id = request.requestId; this.request = request; this.result = result; this.raidMetrics = raidMetrics; @@ -217,6 +224,11 @@ export class SimResult { } static async makeNew(request: RaidSimRequest, result: RaidSimResult): Promise { + const id = request.requestId; + + const cachedResult = simResultsCache.get(id); + if (cachedResult) return cachedResult; + const resultData = new SimResultData(request, result); const logs = await SimLog.parseAll(result); const raidPromise = RaidMetrics.makeNew(resultData, request.raid!, result.raidMetrics!, logs); @@ -225,7 +237,10 @@ export class SimResult { const raidMetrics = await raidPromise; const encounterMetrics = await encounterPromise; - return new SimResult(request, result, raidMetrics, encounterMetrics, logs); + const simResult = new SimResult(request, result, raidMetrics, encounterMetrics, logs); + simResultsCache.set(id, simResult); + + return simResult; } } diff --git a/ui/core/sim.ts b/ui/core/sim.ts index 5c7fb2a500..67bebbc68f 100644 --- a/ui/core/sim.ts +++ b/ui/core/sim.ts @@ -1,4 +1,5 @@ import { hasTouch } from '../shared/bootstrap_overrides'; +import { SimRequest } from '../worker/types'; import { getBrowserLanguageCode, setLanguageCode } from './constants/lang'; import * as OtherConstants from './constants/other'; import { Encounter } from './encounter'; @@ -41,7 +42,7 @@ import { runConcurrentSim, runConcurrentStatWeights } from './sim_concurrent'; import { RequestTypes, SimSignalManager } from './sim_signal_manager'; import { EventID, TypedEvent } from './typed_event.js'; import { getEnumValues, noop } from './utils.js'; -import { WorkerPool, WorkerProgressCallback } from './worker_pool.js'; +import { generateRequestId, WorkerPool, WorkerProgressCallback } from './worker_pool.js'; export type RaidSimData = { request: RaidSimRequest; @@ -266,6 +267,7 @@ export class Sim { // TODO: remove any replenishment from sim request here? probably makes more sense to do it inside the sim to protect against accidents return RaidSimRequest.create({ + requestId: generateRequestId(SimRequest.raidSimAsync), type: this.type, raid: raid, encounter: encounter, diff --git a/ui/core/utils.ts b/ui/core/utils.ts index 46f55f5fdf..b8d8b89cbd 100644 --- a/ui/core/utils.ts +++ b/ui/core/utils.ts @@ -13,6 +13,12 @@ export const existsInDOM = (element: HTMLElement | null) => document.body.contai export const cloneChildren = (element: HTMLElement) => [...(element.childNodes || [])].map(child => child.cloneNode(true)); +export const fragmentToString = (element: Node | Element) => { + const div = document.createElement('div'); + div.appendChild(element.cloneNode(true)); + return div.innerHTML; +}; + export const sanitizeId = (id: string) => id.split(' ').join(''); export const omitDeep = (collection: T, excludeKeys: string[]): T => { diff --git a/ui/core/worker_pool.ts b/ui/core/worker_pool.ts index 17d5470e40..60992e157a 100644 --- a/ui/core/worker_pool.ts +++ b/ui/core/worker_pool.ts @@ -31,7 +31,7 @@ export type WorkerProgressCallback = (progressMetrics: ProgressMetrics) => void; * @param type The request type to prepend. * @returns Random id in the format type-randomhex */ -const generateRequestId = (type: SimRequest) => { +export const generateRequestId = (type: SimRequest) => { const chars = Array.from(Array(4)).map(() => Math.floor(Math.random() * 0x10000).toString(16)); return type + '-' + chars.join(''); }; @@ -149,7 +149,7 @@ export class WorkerPool { async raidSimAsync(request: RaidSimRequest, onProgress: WorkerProgressCallback, signals: SimSignals): Promise { const worker = this.getLeastBusyWorker(); worker.log('Raid sim request: ' + RaidSimRequest.toJsonString(request)); - const id = generateRequestId(SimRequest.raidSimAsync); + const id = request.requestId; signals.abort.onTrigger(async () => { await worker.sendAbortById(id); diff --git a/ui/scss/core/components/detailed_results/_timeline.scss b/ui/scss/core/components/detailed_results/_timeline.scss index 6fbd71674a..424179c949 100644 --- a/ui/scss/core/components/detailed_results/_timeline.scss +++ b/ui/scss/core/components/detailed_results/_timeline.scss @@ -48,7 +48,6 @@ color: var(--bs-holy-power); } - .timeline-root { display: flex; height: 100%; @@ -92,9 +91,47 @@ } } +.timeline-plot { + .apexcharts-tooltip.apexcharts-theme-dark { + box-shadow: none; + border-radius: 0; + background-color: var(--bs-tooltip-bg); + border: var(--bs-tooltip-border) 1px solid; + color: var(--bs-tooltip-body-color); + padding: var(--bs-tooltip-body-padding-y) var(--bs-tooltip-body-padding-x); + } +} + +.timeline-tooltip { + display: flex; + flex-direction: column; + gap: var(--spacer-3); + + ul { + display: flex; + flex-direction: column; + gap: var(--spacer-1); + padding-left: 0; + list-style: none; + margin-bottom: 0; + } + + li { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--spacer-2); + } +} + .timeline-tooltip-header { border-bottom: 1px solid var(--bs-white); - margin-bottom: 5px; +} + +.timeline-tooltip-body { + display: flex; + flex-direction: column; + gap: var(--spacer-3); } .timeline-tooltip-body-row { @@ -113,7 +150,6 @@ .timeline-tooltip-auras { border-top: 1px solid var(--bs-white); - margin-top: 5px; } .rotation-container { @@ -210,7 +246,6 @@ background-color: var(--bs-holy-power); width: 24px; } - } .rotation-timeline-cast { position: absolute; From d9f9772c747c8b709ce38dc61601adaee94ebd7b Mon Sep 17 00:00:00 2001 From: Adrian Klingen Date: Mon, 2 Dec 2024 10:58:00 +0100 Subject: [PATCH 2/2] Fix keysToKeep unused --- ui/core/cache_handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/core/cache_handler.ts b/ui/core/cache_handler.ts index 0cc6ccd326..dd3f0e0dde 100644 --- a/ui/core/cache_handler.ts +++ b/ui/core/cache_handler.ts @@ -24,9 +24,9 @@ export class CacheHandler { } private keepMostRecent() { - if (this.data.size > 2) { + if (this.keysToKeep && this.data.size > this.keysToKeep) { const keys = [...this.data.keys()]; - const keysToRemove = keys.slice(0, keys.length - 2); + const keysToRemove = keys.slice(0, keys.length - this.keysToKeep); keysToRemove.forEach(key => this.data.delete(key)); } }