Skip to content

Commit 682e13a

Browse files
committed
feat(analyze): concurrent demo analyses
Configurable from the Analyze settings tab. ref #1189
1 parent dcb4a62 commit 682e13a

File tree

14 files changed

+179
-53
lines changed

14 files changed

+179
-53
lines changed

src/common/analyses.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const MAX_CONCURRENT_ANALYSES = 8;

src/node/settings/default-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const defaultSettings: Settings = {
4040
redirectDemoToMatch: false,
4141
},
4242
analyze: {
43+
maxConcurrentAnalyses: 4,
4344
analyzePositions: false,
4445
},
4546
playback: {

src/node/settings/settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type UISettings = {
3939
};
4040

4141
type AnalyzeSettings = {
42+
maxConcurrentAnalyses?: number;
4243
analyzePositions: boolean;
4344
};
4445

src/server/analyses-listener.ts

Lines changed: 71 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import { analyzeDemo } from 'csdm/node/demo/analyze-demo';
1111
import { getSettings } from 'csdm/node/settings/get-settings';
1212
import { getErrorCodeFromError } from './get-error-code-from-error';
1313
import type { ErrorCode } from 'csdm/common/error-code';
14+
import { MAX_CONCURRENT_ANALYSES } from 'csdm/common/analyses';
1415

1516
class AnalysesListener {
1617
private analyses: Analysis[] = [];
17-
private currentAnalysis: Analysis | undefined;
18+
private currentAnalyses: Analysis[] = [];
1819
private outputFolderPath: string; // Folder path where CSV files will be write on the host
1920

2021
public constructor() {
@@ -60,47 +61,66 @@ class AnalysesListener {
6061
}
6162

6263
public getAnalyses = () => {
63-
if (this.currentAnalysis !== undefined) {
64-
return [...this.analyses, this.currentAnalysis];
65-
}
66-
return this.analyses;
64+
return [...this.analyses, ...this.currentAnalyses];
6765
};
6866

6967
public hasAnalysesInProgress = () => {
70-
return this.hasPendingAnalyses() || this.currentAnalysis !== undefined;
68+
return this.hasPendingAnalyses() || this.currentAnalyses.length > 0;
7169
};
7270

7371
public clear() {
7472
this.analyses = [];
75-
this.currentAnalysis = undefined;
73+
this.currentAnalyses = [];
7674
}
7775

7876
private hasPendingAnalyses = () => {
7977
return this.analyses.length > 0;
8078
};
8179

8280
private async loopUntilAnalysesDone() {
83-
if (this.currentAnalysis) {
84-
return;
81+
const promises: Promise<void>[] = [];
82+
83+
const settings = await getSettings();
84+
const maxConcurrentAnalyses = Math.min(
85+
MAX_CONCURRENT_ANALYSES,
86+
settings.analyze.maxConcurrentAnalyses ?? MAX_CONCURRENT_ANALYSES / 2,
87+
);
88+
while (this.analyses.length > 0 && this.currentAnalyses.length < maxConcurrentAnalyses) {
89+
const analysis = this.analyses.shift();
90+
if (analysis) {
91+
this.currentAnalyses.push(analysis);
92+
const analysisPromise = this.processAnalysis(analysis, settings.analyze.analyzePositions)
93+
.catch((error) => {
94+
logger.error('Unhandled error during analysis');
95+
logger.error(error);
96+
})
97+
.finally(() => {
98+
this.currentAnalyses = this.currentAnalyses.filter(
99+
({ demoChecksum }) => demoChecksum !== analysis.demoChecksum,
100+
);
101+
});
102+
103+
promises.push(analysisPromise);
104+
}
85105
}
86106

87-
this.currentAnalysis = this.analyses.shift();
88-
while (this.currentAnalysis) {
89-
await this.processAnalysis(this.currentAnalysis);
90-
this.currentAnalysis = this.analyses.shift();
107+
if (promises.length > 0) {
108+
await Promise.race(promises);
109+
if (this.analyses.length > 0) {
110+
await this.loopUntilAnalysesDone();
111+
}
91112
}
92113
}
93114

94-
private readonly processAnalysis = async (analysis: Analysis) => {
115+
private readonly processAnalysis = async (analysis: Analysis, analyzePositions: boolean) => {
95116
const { demoChecksum: checksum, demoPath, source } = analysis;
96117
try {
97-
this.updateCurrentAnalysisStatus(AnalysisStatus.Analyzing);
98-
const settings = await getSettings();
118+
this.updateAnalysisStatus(analysis, AnalysisStatus.Analyzing);
99119
await analyzeDemo({
100120
demoPath,
101-
outputFolderPath: this.outputFolderPath,
121+
outputFolderPath: this.getAnalysisOutputFolderPath(analysis),
102122
source,
103-
analyzePositions: settings.analyze.analyzePositions,
123+
analyzePositions,
104124
onStdout: (data) => {
105125
logger.log(data);
106126
analysis.output += data;
@@ -118,72 +138,72 @@ class AnalysesListener {
118138
});
119139
},
120140
});
121-
this.updateCurrentAnalysisStatus(AnalysisStatus.AnalyzeSuccess);
141+
this.updateAnalysisStatus(analysis, AnalysisStatus.AnalyzeSuccess);
122142

123-
await this.insertMatch(checksum, demoPath);
143+
await this.insertMatch(analysis, checksum, demoPath);
124144
} catch (error) {
125145
logger.error('Error while analyzing demo');
126146
if (error) {
127147
logger.error(error);
128148
}
129149
const isCorruptedDemo = error instanceof CorruptedDemoError;
130-
if (!isCorruptedDemo && this.currentAnalysis && error instanceof Error) {
131-
this.currentAnalysis.output += error.message;
150+
if (!isCorruptedDemo && error instanceof Error) {
151+
analysis.output += error.message;
132152
}
133-
this.updateCurrentAnalysisStatus(AnalysisStatus.AnalyzeError);
153+
this.updateAnalysisStatus(analysis, AnalysisStatus.AnalyzeError);
134154
// If the demo is corrupted, we still want to try to insert it in the database.
135155
if (isCorruptedDemo) {
136-
await this.insertMatch(checksum, demoPath);
156+
await this.insertMatch(analysis, checksum, demoPath);
137157
}
138158
}
139159
};
140160

141-
private async insertMatch(checksum: string, demoPath: string) {
161+
private async insertMatch(analysis: Analysis, checksum: string, demoPath: string) {
142162
try {
143-
this.updateCurrentAnalysisStatus(AnalysisStatus.Inserting);
163+
this.updateAnalysisStatus(analysis, AnalysisStatus.Inserting);
144164
const match = await processMatchInsertion({
145165
checksum,
146166
demoPath,
147-
outputFolderPath: this.outputFolderPath,
167+
outputFolderPath: this.getAnalysisOutputFolderPath(analysis),
148168
});
149-
this.updateCurrentAnalysisStatus(AnalysisStatus.InsertSuccess);
169+
this.updateAnalysisStatus(analysis, AnalysisStatus.InsertSuccess);
150170
server.sendMessageToRendererProcess({
151171
name: RendererServerMessageName.MatchInserted,
152172
payload: match,
153173
});
154174
} catch (error) {
155175
logger.error('Error while inserting match');
156176
logger.error(error);
157-
if (this.currentAnalysis) {
158-
let errorOutput: string;
159-
if (error instanceof Error) {
160-
errorOutput = error.message;
161-
if (error.stack) {
162-
errorOutput += `\n${error.stack}`;
163-
}
164-
if (error.cause) {
165-
errorOutput += `\n${error.cause}`;
166-
}
167-
} else {
168-
errorOutput = String(error);
177+
let errorOutput: string;
178+
if (error instanceof Error) {
179+
errorOutput = error.message;
180+
if (error.stack) {
181+
errorOutput += `\n${error.stack}`;
182+
}
183+
if (error.cause) {
184+
errorOutput += `\n${error.cause}`;
169185
}
170-
this.currentAnalysis.output += errorOutput;
186+
} else {
187+
errorOutput = String(error);
171188
}
189+
analysis.output += errorOutput;
172190

173-
this.updateCurrentAnalysisStatus(AnalysisStatus.InsertError, getErrorCodeFromError(error));
191+
this.updateAnalysisStatus(analysis, AnalysisStatus.InsertError, getErrorCodeFromError(error));
174192
}
175193
}
176194

177-
private updateCurrentAnalysisStatus = (status: AnalysisStatus, errorCode?: ErrorCode) => {
178-
if (this.currentAnalysis !== undefined) {
179-
this.currentAnalysis.status = status;
180-
this.currentAnalysis.errorCode = errorCode;
181-
server.sendMessageToRendererProcess({
182-
name: RendererServerMessageName.AnalysisUpdated,
183-
payload: this.currentAnalysis,
184-
});
185-
}
195+
private updateAnalysisStatus = (analysis: Analysis, status: AnalysisStatus, errorCode?: ErrorCode) => {
196+
analysis.status = status;
197+
analysis.errorCode = errorCode;
198+
server.sendMessageToRendererProcess({
199+
name: RendererServerMessageName.AnalysisUpdated,
200+
payload: analysis,
201+
});
186202
};
203+
204+
private getAnalysisOutputFolderPath(analysis: Analysis) {
205+
return path.join(this.outputFolderPath, analysis.demoChecksum);
206+
}
187207
}
188208

189209
export const analysesListener = new AnalysesListener();

src/ui/match/video/ffmpeg/ffmpeg-audio-bitrate-select.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export function FfmpegAudioBitrateSelect() {
1010
const options: SelectOption<number>[] = [128, 256, 320].map((bitrate) => {
1111
return {
1212
value: bitrate,
13-
label: bitrate.toString(),
13+
label: bitrate,
1414
};
1515
});
1616

@@ -25,7 +25,7 @@ export function FfmpegAudioBitrateSelect() {
2525
onChange={async (audioBitrate) => {
2626
await updateSettings({
2727
ffmpegSettings: {
28-
audioBitrate,
28+
audioBitrate: Number(audioBitrate),
2929
},
3030
});
3131
}}

src/ui/settings/analyze/analyze-settings.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React from 'react';
22
import { SettingsView } from 'csdm/ui/settings/settings-view';
33
import { ToggleAnalyzePositions } from './toggle-analyze-positions';
4+
import { MaxConcurrentAnalysesSelect } from './max-concurrent-analyses-select';
45

56
export function AnalyzeSettings() {
67
return (
78
<SettingsView>
9+
<MaxConcurrentAnalysesSelect />
810
<ToggleAnalyzePositions />
911
</SettingsView>
1012
);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import React from 'react';
2+
import { Trans } from '@lingui/react/macro';
3+
import type { SelectOption } from 'csdm/ui/components/inputs/select';
4+
import { Select } from 'csdm/ui/components/inputs/select';
5+
import { SettingsEntry } from 'csdm/ui/settings/settings-entry';
6+
import { useUpdateSettings } from '../use-update-settings';
7+
import { useAnalyzeSettings } from './use-analyze-settings';
8+
import { MAX_CONCURRENT_ANALYSES } from 'csdm/common/analyses';
9+
10+
export function MaxConcurrentAnalysesSelect() {
11+
const { maxConcurrentAnalyses } = useAnalyzeSettings();
12+
const updateSettings = useUpdateSettings();
13+
14+
const options: SelectOption<number>[] = Array.from({ length: MAX_CONCURRENT_ANALYSES }, (n, i) => ({
15+
value: i + 1,
16+
label: i + 1,
17+
}));
18+
19+
return (
20+
<SettingsEntry
21+
interactiveComponent={
22+
<Select
23+
options={options}
24+
value={maxConcurrentAnalyses}
25+
onChange={async (maxConcurrentAnalyses) => {
26+
await updateSettings({
27+
analyze: {
28+
maxConcurrentAnalyses: Number(maxConcurrentAnalyses),
29+
},
30+
});
31+
}}
32+
/>
33+
}
34+
description={<Trans>Maximum number of concurrent analyses.</Trans>}
35+
title={<Trans context="Settings title">Maximum number of analyses that can run at the same time.</Trans>}
36+
/>
37+
);
38+
}

src/ui/translations/de/messages.po

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6811,6 +6811,15 @@ msgstr ""
68116811
msgid "Ignore bans happened before first match"
68126812
msgstr ""
68136813

6814+
#: src/ui/settings/analyze/max-concurrent-analyses-select.tsx
6815+
msgid "Maximum number of concurrent analyses."
6816+
msgstr ""
6817+
6818+
#: src/ui/settings/analyze/max-concurrent-analyses-select.tsx
6819+
msgctxt "Settings title"
6820+
msgid "Maximum number of analyses that can run at the same time."
6821+
msgstr ""
6822+
68146823
#: src/ui/settings/analyze/toggle-analyze-positions.tsx
68156824
msgid "Analyze player/grenade positions during analysis. Positions are required only for the 2D viewer. When enabled it increases time to insert matches into database and disk space usage."
68166825
msgstr ""

src/ui/translations/en/messages.po

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6810,6 +6810,15 @@ msgstr "Bans that happened before the player was first seen in a match will be i
68106810
msgid "Ignore bans happened before first match"
68116811
msgstr "Ignore bans happened before first match"
68126812

6813+
#: src/ui/settings/analyze/max-concurrent-analyses-select.tsx
6814+
msgid "Maximum number of concurrent analyses."
6815+
msgstr "Maximum number of concurrent analyses."
6816+
6817+
#: src/ui/settings/analyze/max-concurrent-analyses-select.tsx
6818+
msgctxt "Settings title"
6819+
msgid "Maximum number of analyses that can run at the same time."
6820+
msgstr "Maximum number of analyses that can run at the same time."
6821+
68136822
#: src/ui/settings/analyze/toggle-analyze-positions.tsx
68146823
msgid "Analyze player/grenade positions during analysis. Positions are required only for the 2D viewer. When enabled it increases time to insert matches into database and disk space usage."
68156824
msgstr "Analyze player/grenade positions during analysis. Positions are required only for the 2D viewer. When enabled it increases time to insert matches into database and disk space usage."

src/ui/translations/es/messages.po

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6811,6 +6811,15 @@ msgstr ""
68116811
msgid "Ignore bans happened before first match"
68126812
msgstr ""
68136813

6814+
#: src/ui/settings/analyze/max-concurrent-analyses-select.tsx
6815+
msgid "Maximum number of concurrent analyses."
6816+
msgstr ""
6817+
6818+
#: src/ui/settings/analyze/max-concurrent-analyses-select.tsx
6819+
msgctxt "Settings title"
6820+
msgid "Maximum number of analyses that can run at the same time."
6821+
msgstr ""
6822+
68146823
#: src/ui/settings/analyze/toggle-analyze-positions.tsx
68156824
msgid "Analyze player/grenade positions during analysis. Positions are required only for the 2D viewer. When enabled it increases time to insert matches into database and disk space usage."
68166825
msgstr ""

0 commit comments

Comments
 (0)