Skip to content

Commit 0f74b84

Browse files
committed
fix thread issues and restrict Brave
1 parent d4ad776 commit 0f74b84

File tree

8 files changed

+184
-77
lines changed

8 files changed

+184
-77
lines changed

ui/ceval/css/_settings.scss

+50-2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@
3535
margin-#{$start-direction}: 1ch;
3636
}
3737

38-
input[type='range'] {
38+
input[type='range'],
39+
span {
3940
cursor: pointer;
4041
flex: 1 4 auto;
4142
padding: 0;
42-
height: 1.6em;
43+
height: 21px; // important
4344
width: 100%;
4445
margin: 0 1ch;
4546
}
@@ -81,5 +82,52 @@
8182
outline: 2px solid #bbb;
8283
}
8384
}
85+
86+
span {
87+
position: relative;
88+
89+
input[type='range'] {
90+
height: 100%;
91+
margin: 0;
92+
}
93+
}
94+
95+
.tick {
96+
position: absolute;
97+
top: -12.5px; // 12.5px above the track. track is 21px high
98+
height: 46px; // extend 12.5px below the track
99+
border-color: $c-primary;
100+
101+
div {
102+
position: absolute;
103+
left: -4px; // centers the rotated right angle at x = 0
104+
width: 9px;
105+
height: 9px;
106+
border-left: 3px solid $c-primary;
107+
border-bottom: 3px solid $c-primary;
108+
109+
&:hover {
110+
border-color: mix(#fff, $c-primary, 20%);
111+
}
112+
113+
&.arrow-down {
114+
transform: rotate(-45deg);
115+
top: 0;
116+
}
117+
118+
&.arrow-up {
119+
transform: rotate(135deg);
120+
bottom: 0;
121+
}
122+
}
123+
124+
&.recommended div {
125+
border-color: $c-secondary;
126+
127+
&:hover {
128+
border-color: mix(#fff, $c-secondary, 20%);
129+
}
130+
}
131+
}
84132
}
85133
}

ui/ceval/src/ctrl.ts

+18-13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import throttle from 'common/throttle';
22
import { Engines } from './engines/engines';
33
import { CevalOpts, CevalState, CevalEngine, Work, Step, Hovering, PvBoard, Started } from './types';
4-
import { sanIrreversible, showEngineError } from './util';
4+
import { sanIrreversible, showEngineError, fewerCores, constrain } from './util';
55
import { defaultPosition, setupPosition } from 'chessops/variant';
66
import { parseFen } from 'chessops/fen';
77
import { lichessRules } from 'chessops/compat';
88
import { povChances } from './winningChances';
99
import { prop, readonlyProp, Prop, Toggle, toggle } from 'common';
10-
import { hasFeature } from 'common/device';
1110
import { Result } from '@badrap/result';
1211
import { storedIntProp } from 'common/storage';
1312
import { Rules } from 'chessops';
@@ -176,25 +175,31 @@ export default class CevalCtrl {
176175
return this.worker?.getState() ?? CevalState.Initial;
177176
}
178177

178+
setThreads = (threads: number) => lichess.storage.set('ceval.threads', threads.toString());
179+
179180
threads = () => {
180181
const stored = lichess.storage.get('ceval.threads');
181-
return Math.min(
182-
this.engines.active?.maxThreads ?? 96, // Can haz threadripper?
183-
stored ? parseInt(stored, 10) : Math.ceil(navigator.hardwareConcurrency / 4),
184-
);
182+
const desired = stored ? parseInt(stored) : this.recommendedThreads();
183+
return constrain(desired, { min: this.engines.active?.minThreads ?? 1, max: this.maxThreads() });
185184
};
186185

187-
hashSize = () => {
188-
const stored = lichess.storage.get('ceval.hash-size');
189-
return Math.min(this.engines.active?.maxHash ?? 16, stored ? parseInt(stored, 10) : 16);
190-
};
186+
recommendedThreads = () =>
187+
constrain(navigator.hardwareConcurrency - (navigator.hardwareConcurrency % 2 ? 0 : 1), {
188+
min: this.engines.active?.minThreads ?? 1,
189+
max: this.maxThreads(),
190+
});
191191

192-
setThreads = (threads: number) => lichess.storage.set('ceval.threads', threads.toString());
192+
maxThreads = () =>
193+
fewerCores()
194+
? Math.min(this.engines.active?.maxThreads ?? 32, navigator.hardwareConcurrency)
195+
: this.engines.active?.maxThreads ?? 32;
193196

194197
setHashSize = (hash: number) => lichess.storage.set('ceval.hash-size', hash.toString());
195198

196-
maxThreads = () =>
197-
this.engines.external?.maxThreads ?? (hasFeature('sharedMem') ? navigator.hardwareConcurrency : 1);
199+
hashSize = () => {
200+
const stored = lichess.storage.get('ceval.hash-size');
201+
return Math.min(this.maxHash(), stored ? parseInt(stored, 10) : 16);
202+
};
198203

199204
maxHash = () => this.engines.active?.maxHash ?? 16;
200205

ui/ceval/src/engines/engines.ts

+24-17
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,26 @@
1-
import { BrowserEngineInfo, ExternalEngineInfo, EngineInfo, CevalEngine } from '../types';
1+
import { BrowserEngineInfo, ExternalEngineInfo, EngineInfo, CevalEngine, Requires } from '../types';
22
import CevalCtrl from '../ctrl';
33
import { SimpleEngine } from './simpleEngine';
44
import { StockfishWebEngine } from './stockfishWebEngine';
55
import { ThreadedEngine } from './threadedEngine';
66
import { ExternalEngine } from './externalEngine';
77
import { storedStringProp, StoredProp } from 'common/storage';
8-
import { isAndroid, isIOS, isIPad, hasFeature } from 'common/device';
8+
import { isAndroid, isIOS, isIPad, getFirefoxMajorVersion, features, Feature } from 'common/device';
99
import { xhrHeader } from 'common/xhr';
10-
import { pow2floor } from '../util';
1110
import { lichessRules } from 'chessops/compat';
1211

1312
export class Engines {
14-
private localEngines: BrowserEngineInfo[];
15-
private localEngineMap: Map<string, WithMake>;
16-
private externalEngines: ExternalEngineInfo[];
17-
private selectProp: StoredProp<string>;
1813
private _active: EngineInfo | undefined = undefined;
14+
localEngines: BrowserEngineInfo[];
15+
localEngineMap: Map<string, WithMake>;
16+
externalEngines: ExternalEngineInfo[];
17+
selectProp: StoredProp<string>;
18+
browserSupport: Requires[] = features().slice();
1919

2020
constructor(private ctrl: CevalCtrl) {
21+
if ((getFirefoxMajorVersion() ?? 114) > 113 && !('brave' in navigator)) {
22+
this.browserSupport.push('recentFirefoxOrNotBrave');
23+
}
2124
this.localEngineMap = this.makeEngineMap();
2225
this.localEngines = [...this.localEngineMap.values()].map(e => e.info);
2326
this.externalEngines = this.ctrl.opts.externalEngines?.map(e => ({ tech: 'EXTERNAL', ...e })) ?? [];
@@ -40,7 +43,7 @@ export class Engines {
4043
name: 'Fairy Stockfish 14+ NNUE',
4144
short: 'FSF 14+',
4245
tech: 'NNUE',
43-
requires: ['simd', 'webWorkerDynamicImport'],
46+
requires: ['simd', 'recentFirefoxOrNotBrave'],
4447
variants: [key],
4548
assets: {
4649
version: 'sfw003',
@@ -68,7 +71,7 @@ export class Engines {
6871
name: 'Stockfish 16 NNUE · 7MB',
6972
short: 'SF 16 · 7MB',
7073
tech: 'NNUE',
71-
requires: ['simd', 'webWorkerDynamicImport'],
74+
requires: ['simd', 'recentFirefoxOrNotBrave'],
7275
minMem: 1536,
7376
assets: {
7477
version: 'sfw003',
@@ -84,7 +87,7 @@ export class Engines {
8487
name: 'Stockfish 16 NNUE · 40MB',
8588
short: 'SF 16 · 40MB',
8689
tech: 'NNUE',
87-
requires: ['simd', 'webWorkerDynamicImport'],
90+
requires: ['simd', 'recentFirefoxOrNotBrave'],
8891
minMem: 2048,
8992
assets: {
9093
version: 'sfw003',
@@ -118,7 +121,7 @@ export class Engines {
118121
name: 'Fairy Stockfish 14+ HCE',
119122
short: 'FSF 14+',
120123
tech: 'HCE',
121-
requires: ['simd', 'webWorkerDynamicImport'],
124+
requires: ['simd', 'recentFirefoxOrNotBrave'],
122125
variants: variants.map(v => v[0]),
123126
assets: {
124127
version: 'sfw003',
@@ -136,6 +139,7 @@ export class Engines {
136139
short: 'SF 11 MV',
137140
tech: 'HCE',
138141
requires: ['sharedMem'],
142+
minThreads: 1,
139143
variants: variants.map(v => v[0]),
140144
assets: {
141145
version: 'a022fa',
@@ -156,6 +160,7 @@ export class Engines {
156160
short: 'SF 11',
157161
tech: 'HCE',
158162
requires: ['sharedMem'],
163+
minThreads: 1,
159164
assets: {
160165
version: 'a022fa',
161166
root: 'npm/stockfish.wasm',
@@ -171,6 +176,7 @@ export class Engines {
171176
name: 'Stockfish WASM',
172177
short: 'Stockfish',
173178
tech: 'HCE',
179+
minThreads: 1,
174180
maxThreads: 1,
175181
requires: ['wasm'],
176182
obsoletedBy: 'sharedMem',
@@ -188,6 +194,7 @@ export class Engines {
188194
name: 'Stockfish JS',
189195
short: 'Stockfish',
190196
tech: 'HCE',
197+
minThreads: 1,
191198
maxThreads: 1,
192199
obsoletedBy: 'wasm',
193200
assets: {
@@ -201,8 +208,8 @@ export class Engines {
201208
]
202209
.filter(
203210
e =>
204-
e.info.requires?.map(req => hasFeature(req)).every(x => !!x) &&
205-
!(e.info.obsoletedBy && hasFeature(e.info.obsoletedBy)),
211+
e.info.requires?.every((req: Requires) => this.browserSupport.includes(req)) &&
212+
!(e.info.obsoletedBy && this.browserSupport.includes(e.info.obsoletedBy as Feature)),
206213
)
207214
.map(e => [e.info.id, { info: withDefaults(e.info as BrowserEngineInfo), make: e.make }]),
208215
);
@@ -268,11 +275,10 @@ export class Engines {
268275
}
269276

270277
function maxHashMB() {
271-
if (navigator.deviceMemory) return pow2floor(navigator.deviceMemory * 128); // chrome/edge/opera
272-
else if (isAndroid()) return 64; // budget androids are easy to crash @ 128
278+
if (isAndroid()) return 64; // budget androids are easy to crash @ 128
273279
else if (isIPad()) return 64; // iPadOS safari pretends to be desktop but acts more like iphone
274280
else if (isIOS()) return 32;
275-
return 256; // this is safe, mostly desktop firefox / mac safari users here
281+
return 512; // allocating 1024 often fails and offers little benefit over 512, or 16 for that matter
276282
}
277283
const maxHash = maxHashMB();
278284

@@ -286,9 +292,10 @@ function externalEngineSupports(e: EngineInfo, v: VariantKey) {
286292

287293
const withDefaults = (engine: BrowserEngineInfo): BrowserEngineInfo => ({
288294
variants: ['standard', 'chess960', 'fromPosition'],
289-
maxThreads: navigator.hardwareConcurrency ?? 1,
290295
minMem: 1024,
291296
maxHash,
297+
minThreads: 2,
298+
maxThreads: 32,
292299
...engine,
293300
});
294301

ui/ceval/src/types.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ export interface EngineInfo {
3131
tech?: 'HCE' | 'NNUE' | 'EXTERNAL';
3232
short?: string;
3333
variants?: VariantKey[];
34+
minThreads?: number;
3435
maxThreads?: number;
3536
maxHash?: number;
36-
requires?: Feature[];
37+
requires?: Requires[];
3738
}
3839

3940
export interface ExternalEngineInfo extends EngineInfo {
@@ -45,9 +46,11 @@ export interface ExternalEngineInfo extends EngineInfo {
4546
export interface BrowserEngineInfo extends EngineInfo {
4647
minMem?: number;
4748
assets: { root?: string; js?: string; wasm?: string; version?: string; nnue?: string };
48-
obsoletedBy?: 'sharedMem' | 'wasm';
49+
obsoletedBy?: Feature;
4950
}
5051

52+
export type Requires = Feature | 'recentFirefoxOrNotBrave';
53+
5154
export type EngineNotifier = (status?: {
5255
download?: { bytes: number; total: number };
5356
error?: string;

ui/ceval/src/util.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { type Dialog, domDialog } from 'common/dialog';
2+
import { isMobile } from 'common/device';
3+
import { memoize } from 'common/common';
24

35
export function isEvalBetter(a: Tree.ClientEval, b: Tree.ClientEval): boolean {
46
return a.depth > b.depth || (a.depth === b.depth && a.nodes > b.nodes);
@@ -17,11 +19,12 @@ export function sanIrreversible(variant: VariantKey, san: string): boolean {
1719
return variant === 'threeCheck' && san.includes('+');
1820
}
1921

20-
export const pow2floor = (n: number) => {
21-
let pow2 = 1;
22-
while (pow2 * 2 <= n) pow2 *= 2;
23-
return pow2;
24-
};
22+
export function constrain(n: number, constraints: { min?: number; max?: number }): number {
23+
const min = constraints.min ?? n;
24+
const max = constraints.max ?? n;
25+
return Math.max(min, Math.min(max, n));
26+
}
27+
export const fewerCores = memoize<boolean>(() => isMobile() || navigator.userAgent.includes('CrOS'));
2528

2629
export const sharedWasmMemory = (lo: number, hi = 32767): WebAssembly.Memory => {
2730
let shrink = 4; // 32767 -> 24576 -> 16384 -> 12288 -> 8192 -> 6144 -> etc

0 commit comments

Comments
 (0)