Skip to content

Commit 0b984fc

Browse files
josharianpokey
andauthored
optimize getTokenRemainingHatCandidates (#1838)
Object.entries + map allocates a copy of the list twice. And it ends up calling a tiny function over and over. Using a boring nested for loop and an array cuts hat allocation time by about 35%. Removing items from an array one at a time is quadratic (triangular). However, N is capped at the number of hats a user has (per grapheme), which is fundamentally capped at a fairly small number. And copying an array is typically a highly optimized operation. This bit of hot code showed up in a profile from user saidelike on Slack. I have tested that this does not modify behavior on a branch with golden hat tests. ## Checklist - [/] I have added [tests](https://www.cursorless.org/docs/contributing/test-case-recorder/) - [/] I have updated the [docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and [cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet) - [/] I have not broken the cheatsheet --------- Co-authored-by: Pokey Rule <[email protected]>
1 parent 6e2b5ff commit 0b984fc

File tree

1 file changed

+36
-20
lines changed

1 file changed

+36
-20
lines changed

packages/cursorless-engine/src/util/allocateHats/allocateHats.ts

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
Token,
1010
TokenHat,
1111
} from "@cursorless/common";
12-
import { clone } from "lodash";
1312
import { Grapheme, TokenGraphemeSplitter } from "../../tokenGraphemeSplitter";
1413
import { chooseTokenHat } from "./chooseTokenHat";
1514
import { getHatRankingContext } from "./getHatRankingContext";
@@ -69,13 +68,19 @@ export function allocateHats(
6968
tokenGraphemeSplitter,
7069
);
7170

71+
/* All initially enabled hat styles. */
72+
const enabledHatStyleNames = Object.keys(enabledHatStyles);
73+
7274
/**
7375
* A map from graphemes to the remaining hat styles that have not yet been
7476
* used for that grapheme. As we assign hats to tokens, we remove them from
75-
* these lists so that they don't get used again in this pass.
77+
* these arrays so that they don't get used again in this pass.
78+
* We use an array rather than a map to make iteration cheaper;
79+
* we only remove elements once, but we iterate many times,
80+
* and this is a hot path.
7681
*/
77-
const graphemeRemainingHatCandidates = new DefaultMap<string, HatStyleMap>(
78-
() => clone(enabledHatStyles),
82+
const graphemeRemainingHatCandidates = new DefaultMap<string, HatStyleName[]>(
83+
() => [...enabledHatStyleNames],
7984
);
8085

8186
// Iterate through tokens in order of decreasing rank, assigning each one a
@@ -90,6 +95,7 @@ export function allocateHats(
9095
tokenGraphemeSplitter,
9196
token,
9297
graphemeRemainingHatCandidates,
98+
enabledHatStyles,
9399
);
94100

95101
const chosenHat = chooseTokenHat(
@@ -107,10 +113,12 @@ export function allocateHats(
107113
}
108114

109115
// Remove the hat we chose from consideration for lower ranked tokens
110-
delete graphemeRemainingHatCandidates.get(chosenHat.grapheme.text)[
111-
chosenHat.style
112-
];
113-
116+
graphemeRemainingHatCandidates.set(
117+
chosenHat.grapheme.text,
118+
graphemeRemainingHatCandidates
119+
.get(chosenHat.grapheme.text)
120+
.filter((style) => style !== chosenHat.style),
121+
);
114122
return constructHatRangeDescriptor(token, chosenHat);
115123
})
116124
.filter((value): value is TokenHat => value != null);
@@ -135,19 +143,27 @@ function getTokenOldHatMap(oldTokenHats: readonly TokenHat[]) {
135143
function getTokenRemainingHatCandidates(
136144
tokenGraphemeSplitter: TokenGraphemeSplitter,
137145
token: Token,
138-
availableGraphemeStyles: DefaultMap<string, HatStyleMap>,
146+
graphemeRemainingHatCandidates: DefaultMap<string, HatStyleName[]>,
147+
enabledHatStyles: HatStyleMap,
139148
): HatCandidate[] {
140-
return tokenGraphemeSplitter
141-
.getTokenGraphemes(token.text)
142-
.flatMap((grapheme) =>
143-
Object.entries(availableGraphemeStyles.get(grapheme.text)).map(
144-
([style, { penalty }]) => ({
145-
grapheme,
146-
style,
147-
penalty,
148-
}),
149-
),
150-
);
149+
// Use iteration here instead of functional constructs,
150+
// because this is a hot path and we want to avoid allocating arrays
151+
// and calling tiny functions lots of times.
152+
const candidates: HatCandidate[] = [];
153+
const graphemes = tokenGraphemeSplitter.getTokenGraphemes(token.text);
154+
for (const grapheme of graphemes) {
155+
for (const style of graphemeRemainingHatCandidates.get(grapheme.text)) {
156+
// Allocating and pushing all of these objects is
157+
// the single most expensive thing in hat allocation.
158+
// Please pay attention to performance when modifying this code.
159+
candidates.push({
160+
grapheme,
161+
style,
162+
penalty: enabledHatStyles[style].penalty,
163+
});
164+
}
165+
}
166+
return candidates;
151167
}
152168

153169
/**

0 commit comments

Comments
 (0)