Skip to content

Commit 7733112

Browse files
authored
Merge pull request #1103 from mathjax/fix/performance_issues
Improve performance by detaching speech computation
2 parents f506131 + 6e9bb8f commit 7733112

File tree

5 files changed

+76
-16
lines changed

5 files changed

+76
-16
lines changed

ts/a11y/explorer/KeyExplorer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525

2626
import {A11yDocument, HoverRegion, SpeechRegion, LiveRegion} from './Region.js';
27+
import {STATE} from '../../core/MathItem.js';
2728
import type { ExplorerMathItem } from '../explorer.js';
2829
import {Explorer, AbstractExplorer} from './Explorer.js';
2930
import {ExplorerPool} from './ExplorerPool.js';
@@ -446,6 +447,10 @@ export class SpeechExplorer extends AbstractExplorer<string> implements KeyExplo
446447
* @override
447448
*/
448449
public Start() {
450+
// In case the speech is not attached yet, we generate it
451+
if (this.item.state() < STATE.ATTACHSPEECH) {
452+
this.item.attachSpeech(this.document);
453+
};
449454
if (!this.attached) return;
450455
if (this.node.hasAttribute('tabindex')) {
451456
this.node.removeAttribute('tabindex');
@@ -458,7 +463,7 @@ export class SpeechExplorer extends AbstractExplorer<string> implements KeyExplo
458463
this.current = this.node.querySelector(`[data-semantic-id="${this.restarted}"]`)
459464
if (!this.current) {
460465
const dummies = Array.from(
461-
this.node.querySelectorAll(`[data-semantic-type="dummy"]`))
466+
this.node.querySelectorAll('[data-semantic-type="dummy"]'))
462467
.map(x => x.getAttribute('data-semantic-id'))
463468
let internal = this.generators.element.querySelector(
464469
`[data-semantic-id="${this.restarted}"]`);

ts/a11y/semantic-enrich.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {SerializedMmlVisitor} from '../core/MmlTree/SerializedMmlVisitor.js';
3131
import {OptionList, expandable} from '../util/Options.js';
3232
import {Sre} from './sre.js';
3333
import { buildSpeech } from './speech/SpeechUtil.js';
34-
3534
import { GeneratorPool } from './speech/GeneratorPool.js';
3635

3736
/*==========================================================================*/
@@ -46,12 +45,12 @@ export type Constructor<T> = new(...args: any[]) => T;
4645
/**
4746
* Add STATE value for being enriched (after COMPILED and before TYPESET)
4847
*/
49-
newState('ENRICHED', 30);
48+
newState('ENRICHED', STATE.COMPILED + 10);
5049

5150
/**
52-
* Add STATE value for adding speech (after TYPESET)
51+
* Add STATE value for adding speech (after INSERTED)
5352
*/
54-
newState('ATTACHSPEECH', 155);
53+
newState('ATTACHSPEECH', STATE.INSERTED + 10);
5554

5655
/*==========================================================================*/
5756

@@ -243,7 +242,7 @@ export function EnrichedMathItemMixin<N, T, D, B extends Constructor<AbstractMat
243242
*
244243
* @return {[string, string]} Pair comprising speech and braille.
245244
*/
246-
private existingSpeech(): [string, string] {
245+
protected existingSpeech(): [string, string] {
247246
const attributes = this.root.attributes;
248247
let speech = attributes.get('aria-label') as string;
249248
if (!speech) {
@@ -379,6 +378,11 @@ export function EnrichedMathDocumentMixin<N, T, D, B extends MathDocumentConstru
379378
enrich: [STATE.ENRICHED],
380379
attachSpeech: [STATE.ATTACHSPEECH]
381380
}),
381+
speechTiming: {
382+
initial: 100, // initial delay until starting to add speech
383+
threshold: 250, // time (in milliseconds) to process speech before letting screen update
384+
intermediate: 10 // delay after processing speech reaches the threshold
385+
},
382386
sre: expandable({
383387
speech: 'none', // by default no speech is included
384388
locale: 'en', // switch the locale
@@ -392,6 +396,16 @@ export function EnrichedMathDocumentMixin<N, T, D, B extends MathDocumentConstru
392396
})
393397
};
394398

399+
/**
400+
* The list of MathItems that need to be processed for speech
401+
*/
402+
protected awaitingSpeech: MathItem<N, T, D>[];
403+
404+
/**
405+
* The identifier from setTimeout for the next speech loop
406+
*/
407+
protected speechTimeout: number = 0;
408+
395409
/**
396410
* Enrich the MathItem class used for this MathDocument, and create the
397411
* temporary MathItem used for enrchment
@@ -421,15 +435,37 @@ export function EnrichedMathDocumentMixin<N, T, D, B extends MathDocumentConstru
421435
public attachSpeech() {
422436
if (!this.processed.isSet('attach-speech')) {
423437
if (this.options.enableSpeech || this.options.enableBraille) {
424-
for (const math of this.math) {
425-
(math as EnrichedMathItem<N, T, D>).attachSpeech(this);
438+
if (this.speechTimeout) {
439+
clearTimeout(this.speechTimeout);
440+
this.speechTimeout = 0;
426441
}
442+
this.awaitingSpeech = Array.from(this.math);
443+
this.speechTimeout = setTimeout(
444+
() => this.attachSpeechLoop(),
445+
this.options.speechTiming.initial
446+
);
427447
}
428448
this.processed.set('attach-speech');
429449
}
430450
return this;
431451
}
432452

453+
/**
454+
* Loops through math items to attach speech until the timeout threshold is reached.
455+
*/
456+
protected attachSpeechLoop() {
457+
const timing = this.options.speechTiming;
458+
const awaitingSpeech = this.awaitingSpeech;
459+
const timeStart = new Date().getTime();
460+
const timeEnd = timeStart + timing.threshold;
461+
do {
462+
const math = awaitingSpeech.shift();
463+
(math as EnrichedMathItem<N, T, D>).attachSpeech(this);
464+
} while (awaitingSpeech.length && new Date().getTime() < timeEnd);
465+
this.speechTimeout = awaitingSpeech.length ?
466+
setTimeout(() => this.attachSpeechLoop(), timing.intermediate) : 0;
467+
}
468+
433469
/**
434470
* Enrich the MathItems in this MathDocument
435471
*/

ts/a11y/speech/GeneratorPool.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,9 @@ export class GeneratorPool<N, T, D> {
161161
public computeSpeech(node: N, mml: string): [string, string] {
162162
this.element = Sre.parseDOM(mml);
163163
const xml = this.prepareXml(node);
164-
const speech = this.speechGenerator.getSpeech(xml, this.element);
165-
const braille = this.brailleGenerator.getSpeech(xml, this.element);
166-
if (this.options.enableSpeech || this.options.enableBraille) {
164+
const speech = this.options.enableSpeech ? this.speechGenerator.getSpeech(xml, this.element) : '';
165+
const braille = this.options.enableBraille ? this.brailleGenerator.getSpeech(xml, this.element) : '';
166+
if (speech || braille) {
167167
this.setAria(node, xml, this.options.sre.locale);
168168
}
169169
return [speech, braille];

ts/a11y/speech/SpeechUtil.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ export function buildLabel(
191191
* @param {string} speech The speech string.
192192
* @param {string=} locale An optional locale.
193193
* @param {string=} rate The base speech rate.
194-
* @return {[string, SsmlElement[]]} The speech with the ssml annotation structure
194+
* @return {[string, SsmlElement[]]} The speech with the ssml annotation structure
195195
*/
196196
export function buildSpeech(speech: string, locale: string = 'en',
197197
rate: string = '100'): [string, SsmlElement[]] {

ts/ui/lazy/LazyHandler.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323

2424
import {MathDocumentConstructor, ContainerList} from '../../core/MathDocument.js';
2525
import {MathItem, STATE, newState} from '../../core/MathItem.js';
26-
import {HTMLMathItem} from '../../handlers/html/HTMLMathItem.js';
2726
import {HTMLDocument} from '../../handlers/html/HTMLDocument.js';
2827
import {HTMLHandler} from '../../handlers/html/HTMLHandler.js';
28+
import {EnrichedMathItem} from '../../a11y/semantic-enrich.js';
2929
import {handleRetriesFor} from '../../util/Retries.js';
3030
import {OptionList} from '../../util/Options.js';
3131

@@ -151,7 +151,7 @@ export interface LazyMathItem<N, T, D> extends MathItem<N, T, D> {
151151
* @template D The Document class
152152
* @template B The MathItem class to extend
153153
*/
154-
export function LazyMathItemMixin<N, T, D, B extends Constructor<HTMLMathItem<N, T, D>>>(
154+
export function LazyMathItemMixin<N, T, D, B extends Constructor<EnrichedMathItem<N, T, D>>>(
155155
BaseMathItem: B
156156
): Constructor<LazyMathItem<N, T, D>> & B {
157157

@@ -260,6 +260,19 @@ export function LazyMathItemMixin<N, T, D, B extends Constructor<HTMLMathItem<N,
260260
}
261261
}
262262

263+
/**
264+
* Only add speech when we are actually typesetting
265+
*
266+
* @override
267+
*/
268+
public attachSpeech = (document: LazyMathDocument<N, T, D>) => {
269+
if (this.state() >= STATE.ATTACHSPEECH) return;
270+
if (!this.lazyTypeset) {
271+
super.attachSpeech?.(document);
272+
}
273+
this.state(STATE.ATTACHSPEECH);
274+
}
275+
263276
};
264277

265278
}
@@ -325,8 +338,14 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
325338
*/
326339
public static OPTIONS: OptionList = {
327340
...BaseDocument.OPTIONS,
328-
lazyMargin: '200px',
341+
lazyMargin: '500px',
329342
lazyAlwaysTypeset: null,
343+
speechTiming: {
344+
...(BaseDocument.OPTIONS.speechTiming || {}),
345+
initial: 150,
346+
threshold: 100,
347+
intermediate: 10
348+
},
330349
renderActions: {
331350
...BaseDocument.OPTIONS.renderActions,
332351
lazyAlways: [STATE.LAZYALWAYS, 'lazyAlways', '', false]
@@ -390,7 +409,7 @@ B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
390409
// Use the LazyMathItem for math items
391410
//
392411
this.options.MathItem =
393-
LazyMathItemMixin<N, T, D, Constructor<HTMLMathItem<N, T, D>>>(this.options.MathItem);
412+
LazyMathItemMixin<N, T, D, Constructor<EnrichedMathItem<N, T, D>>>(this.options.MathItem);
394413
//
395414
// Allocate a process bit for lazyAlways
396415
//

0 commit comments

Comments
 (0)