Skip to content

Commit aaced3c

Browse files
author
Chris Garrett
committed
[BUGFIX] Make the custom tag system non-enumerable
Currently, the custom tag system relies on an enumerable "symbol"-like key, which is then placed on the objects that need to use it. This is an expensive system, since it means we need to do a brand check to see if the function exists, which means crawling the prototype chain every time we look up a tag for a property on an object. This PR instead places the functions in a WeakMap. This means that in general, we can check to see if the WeakMap has the value, and if so then we can use the function directly. WeakMap lookup is generally faster, as it means we don't have to walk the entire prototype chain of a megamorphic object, so this should generally be more performant. It does mean that the functions in question need to be bound to the instance, which could impact performance, but the functions should still be inline-able when called (since they are called independent of the object), and given that usage of tag lookup with non-proxies likely dominates over usage with proxies, it should matter less than the savings from using a WeakMap. Finally, this means that CUSTOM_TAG_FOR is not exposed at all, so there cannot be any observable side-effects such as the ones causing the bug in: ember-m3/ember-m3#957 \### Unrelated Changes In addition, I did a quick refactor of the proxy handlers to extract them as classes. This is important for performance (POJOs with methods on the instance do not inline, even as proxy handlers), I just never got around to it before.
1 parent 5ba5598 commit aaced3c

File tree

5 files changed

+147
-84
lines changed

5 files changed

+147
-84
lines changed

packages/@glimmer/manager/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ export { modifierCapabilities, CustomModifierManager } from './lib/public/modifi
1212
export { helperCapabilities, hasDestroyable, hasValue, customHelper } from './lib/public/helper';
1313
export { getComponentTemplate, setComponentTemplate } from './lib/public/template';
1414
export { capabilityFlagsFrom, hasCapability, managerHasCapability } from './lib/util/capabilities';
15-
export { CUSTOM_TAG_FOR } from './lib/util/args-proxy';
15+
export { getCustomTagFor, setCustomTagFor } from './lib/util/args-proxy';

packages/@glimmer/manager/lib/util/args-proxy.ts

+94-82
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@ import {
66
CapturedPositionalArguments,
77
} from '@glimmer/interfaces';
88
import { Reference, valueForRef } from '@glimmer/reference';
9-
import { HAS_NATIVE_PROXY, enumerableSymbol } from '@glimmer/util';
9+
import { HAS_NATIVE_PROXY } from '@glimmer/util';
1010
import { Tag, track } from '@glimmer/validator';
1111

12-
export const CUSTOM_TAG_FOR = enumerableSymbol('CUSTOM_TAG_FOR');
12+
const CUSTOM_TAG_FOR = new WeakMap<object, (obj: object, key: string) => Tag>();
13+
14+
export function getCustomTagFor(obj: object): ((obj: object, key: string) => Tag) | undefined {
15+
return CUSTOM_TAG_FOR.get(obj);
16+
}
17+
18+
export function setCustomTagFor(obj: object, customTagFn: (obj: object, key: string) => Tag) {
19+
CUSTOM_TAG_FOR.set(obj, customTagFn);
20+
}
1321

1422
function convertToInt(prop: number | string | symbol): number | null {
1523
if (typeof prop === 'symbol') return null;
@@ -50,81 +58,89 @@ export let argsProxyFor: (
5058
type: 'component' | 'helper' | 'modifier'
5159
) => Arguments;
5260

53-
if (HAS_NATIVE_PROXY) {
54-
argsProxyFor = (capturedArgs, type) => {
55-
const { named, positional } = capturedArgs;
61+
class NamedArgsProxy implements ProxyHandler<{}> {
62+
declare set?: (target: {}, prop: string | number | symbol) => boolean;
5663

57-
let getNamedTag = (key: string) => tagForNamedArg(named, key);
58-
let getPositionalTag = (key: string) => tagForPositionalArg(positional, key);
64+
constructor(private named: CapturedNamedArguments) {}
5965

60-
const namedHandler: ProxyHandler<{}> = {
61-
get(_target, prop) {
62-
const ref = named[prop as string];
66+
get(_target: {}, prop: string | number | symbol) {
67+
const ref = this.named[prop as string];
6368

64-
if (ref !== undefined) {
65-
return valueForRef(ref);
66-
} else if (prop === CUSTOM_TAG_FOR) {
67-
return getNamedTag;
68-
}
69-
},
70-
71-
has(_target, prop) {
72-
return prop in named;
73-
},
74-
75-
ownKeys(_target) {
76-
return Object.keys(named);
77-
},
78-
79-
isExtensible() {
80-
return false;
81-
},
82-
83-
getOwnPropertyDescriptor(_target, prop) {
84-
if (DEBUG && !(prop in named)) {
85-
throw new Error(
86-
`args proxies do not have real property descriptors, so you should never need to call getOwnPropertyDescriptor yourself. This code exists for enumerability, such as in for-in loops and Object.keys(). Attempted to get the descriptor for \`${String(
87-
prop
88-
)}\``
89-
);
90-
}
91-
92-
return {
93-
enumerable: true,
94-
configurable: true,
95-
};
96-
},
69+
if (ref !== undefined) {
70+
return valueForRef(ref);
71+
}
72+
}
73+
74+
has(_target: {}, prop: string | number | symbol) {
75+
return prop in this.named;
76+
}
77+
78+
ownKeys() {
79+
return Object.keys(this.named);
80+
}
81+
82+
isExtensible() {
83+
return false;
84+
}
85+
86+
getOwnPropertyDescriptor(_target: {}, prop: string | number | symbol) {
87+
if (DEBUG && !(prop in this.named)) {
88+
throw new Error(
89+
`args proxies do not have real property descriptors, so you should never need to call getOwnPropertyDescriptor yourself. This code exists for enumerability, such as in for-in loops and Object.keys(). Attempted to get the descriptor for \`${String(
90+
prop
91+
)}\``
92+
);
93+
}
94+
95+
return {
96+
enumerable: true,
97+
configurable: true,
9798
};
99+
}
100+
}
98101

99-
const positionalHandler: ProxyHandler<[]> = {
100-
get(target, prop) {
101-
if (prop === 'length') {
102-
return positional.length;
103-
}
102+
class PositionalArgsProxy implements ProxyHandler<[]> {
103+
declare set?: (target: [], prop: string | number | symbol) => boolean;
104+
declare ownKeys?: (target: []) => string[];
104105

105-
const parsed = convertToInt(prop);
106+
constructor(private positional: CapturedPositionalArguments) {}
106107

107-
if (parsed !== null && parsed < positional.length) {
108-
return valueForRef(positional[parsed]);
109-
}
108+
get(target: [], prop: string | number | symbol) {
109+
let { positional } = this;
110110

111-
if (prop === CUSTOM_TAG_FOR) {
112-
return getPositionalTag;
113-
}
111+
if (prop === 'length') {
112+
return positional.length;
113+
}
114114

115-
return (target as any)[prop];
116-
},
115+
const parsed = convertToInt(prop);
117116

118-
isExtensible() {
119-
return false;
120-
},
117+
if (parsed !== null && parsed < positional.length) {
118+
return valueForRef(positional[parsed]);
119+
}
121120

122-
has(_target, prop) {
123-
const parsed = convertToInt(prop);
121+
return (target as any)[prop];
122+
}
124123

125-
return parsed !== null && parsed < positional.length;
126-
},
127-
};
124+
isExtensible() {
125+
return false;
126+
}
127+
128+
has(_target: [], prop: string | number | symbol) {
129+
const parsed = convertToInt(prop);
130+
131+
return parsed !== null && parsed < this.positional.length;
132+
}
133+
}
134+
135+
if (HAS_NATIVE_PROXY) {
136+
argsProxyFor = (capturedArgs, type) => {
137+
const { named, positional } = capturedArgs;
138+
139+
let getNamedTag = (_obj: object, key: string) => tagForNamedArg(named, key);
140+
let getPositionalTag = (_obj: object, key: string) => tagForPositionalArg(positional, key);
141+
142+
const namedHandler = new NamedArgsProxy(named);
143+
const positionalHandler = new PositionalArgsProxy(positional);
128144

129145
const namedTarget = Object.create(null);
130146
const positionalTarget: unknown[] = [];
@@ -149,25 +165,29 @@ if (HAS_NATIVE_PROXY) {
149165
positionalHandler.ownKeys = forInDebugHandler;
150166
}
151167

168+
const namedProxy = new Proxy(namedTarget, namedHandler);
169+
const positionalProxy = new Proxy(positionalTarget, positionalHandler);
170+
171+
setCustomTagFor(namedProxy, getNamedTag);
172+
setCustomTagFor(positionalProxy, getPositionalTag);
173+
152174
return {
153-
named: new Proxy(namedTarget, namedHandler),
154-
positional: new Proxy(positionalTarget, positionalHandler),
175+
named: namedProxy,
176+
positional: positionalProxy,
155177
};
156178
};
157179
} else {
158180
argsProxyFor = (capturedArgs, _type) => {
159181
const { named, positional } = capturedArgs;
160182

161-
let getNamedTag = (key: string) => tagForNamedArg(named, key);
162-
let getPositionalTag = (key: string) => tagForPositionalArg(positional, key);
183+
let getNamedTag = (_obj: object, key: string) => tagForNamedArg(named, key);
184+
let getPositionalTag = (_obj: object, key: string) => tagForPositionalArg(positional, key);
163185

164186
let namedProxy = {};
187+
let positionalProxy: unknown[] = [];
165188

166-
Object.defineProperty(namedProxy, CUSTOM_TAG_FOR, {
167-
configurable: false,
168-
enumerable: false,
169-
value: getNamedTag,
170-
});
189+
setCustomTagFor(namedProxy, getNamedTag);
190+
setCustomTagFor(positionalProxy, getPositionalTag);
171191

172192
Object.keys(named).forEach((name) => {
173193
Object.defineProperty(namedProxy, name, {
@@ -179,14 +199,6 @@ if (HAS_NATIVE_PROXY) {
179199
});
180200
});
181201

182-
let positionalProxy: unknown[] = [];
183-
184-
Object.defineProperty(positionalProxy, CUSTOM_TAG_FOR, {
185-
configurable: false,
186-
enumerable: false,
187-
value: getPositionalTag,
188-
});
189-
190202
positional.forEach((ref: Reference, index: number) => {
191203
Object.defineProperty(positionalProxy, index, {
192204
enumerable: true,

packages/@glimmer/util/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export * from './lib/template';
2323
export { default as _WeakSet } from './lib/weak-set';
2424
export { castToSimple, castToBrowser, checkNode } from './lib/simple-cast';
2525
export * from './lib/present';
26+
export { default as intern } from './lib/intern';
2627

2728
export { default as debugToString } from './lib/debug-to-string';
2829
export { beginTestSteps, endTestSteps, logStep, verifySteps } from './lib/debug-steps';

packages/@glimmer/util/lib/intern.ts

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
Strongly hint runtimes to intern the provided string.
3+
4+
When do I need to use this function?
5+
6+
For the most part, never. Pre-mature optimization is bad, and often the
7+
runtime does exactly what you need it to, and more often the trade-off isn't
8+
worth it.
9+
10+
Why?
11+
12+
Runtimes store strings in at least 2 different representations:
13+
Ropes and Symbols (interned strings). The Rope provides a memory efficient
14+
data-structure for strings created from concatenation or some other string
15+
manipulation like splitting.
16+
17+
Unfortunately checking equality of different ropes can be quite costly as
18+
runtimes must resort to clever string comparison algorithms. These
19+
algorithms typically cost in proportion to the length of the string.
20+
Luckily, this is where the Symbols (interned strings) shine. As Symbols are
21+
unique by their string content, equality checks can be done by pointer
22+
comparison.
23+
24+
How do I know if my string is a rope or symbol?
25+
26+
Typically (warning general sweeping statement, but truthy in runtimes at
27+
present) static strings created as part of the JS source are interned.
28+
Strings often used for comparisons can be interned at runtime if some
29+
criteria are met. One of these criteria can be the size of the entire rope.
30+
For example, in chrome 38 a rope longer then 12 characters will not
31+
intern, nor will segments of that rope.
32+
33+
Some numbers: http://jsperf.com/eval-vs-keys/8
34+
35+
Known Trick™
36+
37+
@private
38+
@return {String} interned version of the provided string
39+
*/
40+
export default function intern(str: string): string {
41+
let obj: Record<string, number> = {};
42+
obj[str] = 1;
43+
for (let key in obj) {
44+
if (key === str) {
45+
return key;
46+
}
47+
}
48+
return str;
49+
}

packages/@glimmer/util/lib/platform-utils.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Maybe } from '@glimmer/interfaces';
2+
import intern from './intern';
23

34
export type Factory<T> = new (...args: unknown[]) => T;
45

@@ -40,7 +41,7 @@ export type Lit = string | number | boolean | undefined | null | void | {};
4041
export const tuple = <T extends Lit[]>(...args: T) => args;
4142

4243
export function enumerableSymbol(key: string): any {
43-
return `__${key}${Math.floor(Math.random() * Date.now())}__`;
44+
return intern(`__${key}${Math.floor(Math.random() * Date.now())}__`);
4445
}
4546

4647
export const symbol = HAS_NATIVE_SYMBOL ? Symbol : enumerableSymbol;

0 commit comments

Comments
 (0)