Skip to content

Commit f23a142

Browse files
committed
Optimize primitive color processing
1 parent 066c0d8 commit f23a142

3 files changed

Lines changed: 185 additions & 0 deletions

File tree

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
* @fantom_native_opt false
10+
* @fantom_js_bytecode false
11+
*/
12+
13+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
14+
15+
import processColor from '../processColor';
16+
import * as Fantom from '@react-native/fantom';
17+
18+
const REPEATED_COLORS = [
19+
'red',
20+
'blue',
21+
'#1e83c9',
22+
'rgba(10, 20, 30, 0.4)',
23+
'hsl(318, 69%, 55%)',
24+
];
25+
26+
const GENERATED_COLORS = Array.from(
27+
{length: 2048},
28+
(_, i) =>
29+
`rgba(${(i % 256).toString()}, ${((i * 3) % 256).toString()}, ${(
30+
(i * 7) %
31+
256
32+
).toString()}, ${((i % 100) / 100).toFixed(2)})`,
33+
);
34+
35+
let benchmarkSink = 0;
36+
37+
function processColors(
38+
colors: ReadonlyArray<string>,
39+
iterations: number,
40+
): void {
41+
let result = 0;
42+
for (let i = 0; i < iterations; i++) {
43+
const color = processColor(colors[i % colors.length]);
44+
if (typeof color === 'number') {
45+
result += color;
46+
}
47+
}
48+
benchmarkSink += result;
49+
if (benchmarkSink > Number.MAX_SAFE_INTEGER) {
50+
benchmarkSink = 0;
51+
}
52+
}
53+
54+
Fantom.unstable_benchmark
55+
.suite('processColor')
56+
.test('process repeated primitive colors', () => {
57+
processColors(REPEATED_COLORS, 1000);
58+
})
59+
.test('process generated primitive colors', () => {
60+
processColors(GENERATED_COLORS, 2048);
61+
});

packages/react-native/Libraries/StyleSheet/__tests__/processColor-test.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,39 @@
1010

1111
'use strict';
1212

13+
jest.mock('../../Utilities/NativePlatformConstantsAndroid', () => ({
14+
__esModule: true,
15+
default: {
16+
getConstants: () => ({
17+
reactNativeVersion: {
18+
major: 1000,
19+
minor: 0,
20+
patch: 0,
21+
prerelease: undefined,
22+
},
23+
}),
24+
},
25+
}));
26+
27+
jest.mock('../../Utilities/NativePlatformConstantsIOS', () => ({
28+
__esModule: true,
29+
default: {
30+
getConstants: () => ({
31+
forceTouchAvailable: false,
32+
interfaceIdiom: 'phone',
33+
isTesting: true,
34+
osVersion: '1.0',
35+
reactNativeVersion: {
36+
major: 1000,
37+
minor: 0,
38+
patch: 0,
39+
prerelease: undefined,
40+
},
41+
systemName: 'iOS',
42+
}),
43+
},
44+
}));
45+
1346
const {OS} = require('../../Utilities/Platform').default;
1447
const PlatformColorAndroid =
1548
// $FlowFixMe[missing-platform-support]
@@ -122,4 +155,67 @@ describe('processColor', () => {
122155
});
123156
}
124157
});
158+
159+
describe('primitive color cache', () => {
160+
afterEach(() => {
161+
jest.dontMock('@react-native/normalize-colors');
162+
jest.resetModules();
163+
});
164+
165+
it('should cache processed primitive colors', () => {
166+
jest.resetModules();
167+
168+
const normalizeColorMock = jest.fn(() => 0xff0000ff);
169+
jest.doMock('@react-native/normalize-colors', () => normalizeColorMock);
170+
171+
const cachedProcessColor = require('../processColor').default;
172+
173+
expect(cachedProcessColor('cached-red')).toEqual(
174+
platformSpecific(0xffff0000),
175+
);
176+
expect(cachedProcessColor('cached-red')).toEqual(
177+
platformSpecific(0xffff0000),
178+
);
179+
expect(normalizeColorMock).toHaveBeenCalledTimes(1);
180+
});
181+
182+
it('should cache invalid primitive colors', () => {
183+
jest.resetModules();
184+
185+
const normalizeColorMock = jest.fn(() => undefined);
186+
jest.doMock('@react-native/normalize-colors', () => normalizeColorMock);
187+
188+
const cachedProcessColor = require('../processColor').default;
189+
190+
expect(cachedProcessColor('not-a-color')).toBeUndefined();
191+
expect(cachedProcessColor('not-a-color')).toBeUndefined();
192+
expect(normalizeColorMock).toHaveBeenCalledTimes(1);
193+
});
194+
195+
it('should stop admitting primitive colors after reaching the cache bound', () => {
196+
jest.resetModules();
197+
198+
const normalizeColorMock = jest.fn(() => 0xff0000ff);
199+
jest.doMock('@react-native/normalize-colors', () => normalizeColorMock);
200+
201+
const cachedProcessColor = require('../processColor').default;
202+
203+
for (let i = 0; i < 1024; i++) {
204+
cachedProcessColor(`cached-color-${i.toString()}`);
205+
}
206+
expect(normalizeColorMock).toHaveBeenCalledTimes(1024);
207+
208+
cachedProcessColor('cached-color-0');
209+
expect(normalizeColorMock).toHaveBeenCalledTimes(1024);
210+
211+
cachedProcessColor('cached-color-1024');
212+
expect(normalizeColorMock).toHaveBeenCalledTimes(1025);
213+
214+
cachedProcessColor('cached-color-1024');
215+
expect(normalizeColorMock).toHaveBeenCalledTimes(1026);
216+
217+
cachedProcessColor('cached-color-0');
218+
expect(normalizeColorMock).toHaveBeenCalledTimes(1026);
219+
});
220+
});
125221
});

packages/react-native/Libraries/StyleSheet/processColor.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,30 @@ const normalizeColor = require('./normalizeColor').default;
1717

1818
export type ProcessedColorValue = number | NativeColorValue;
1919

20+
type CacheableColorValue = number | string;
21+
22+
const MAX_PRIMITIVE_COLOR_CACHE_SIZE = 1024;
23+
const primitiveColorCache: Map<CacheableColorValue, ?ProcessedColorValue> =
24+
new Map();
25+
2026
/* eslint no-bitwise: 0 */
2127
function processColor(color?: ?(number | ColorValue)): ?ProcessedColorValue {
2228
if (color === undefined || color === null) {
2329
return color;
2430
}
2531

32+
if (typeof color === 'string' || typeof color === 'number') {
33+
const cachedColor = primitiveColorCache.get(color);
34+
if (cachedColor !== undefined || primitiveColorCache.has(color)) {
35+
return cachedColor;
36+
}
37+
}
38+
2639
let normalizedColor = normalizeColor(color);
2740
if (normalizedColor === null || normalizedColor === undefined) {
41+
if (typeof color === 'string' || typeof color === 'number') {
42+
cachePrimitiveColor(color, undefined);
43+
}
2844
return undefined;
2945
}
3046

@@ -53,7 +69,19 @@ function processColor(color?: ?(number | ColorValue)): ?ProcessedColorValue {
5369
// *unsigned* to *signed* 32bit int that way.
5470
normalizedColor = normalizedColor | 0x0;
5571
}
72+
if (typeof color === 'string' || typeof color === 'number') {
73+
cachePrimitiveColor(color, normalizedColor);
74+
}
5675
return normalizedColor;
5776
}
5877

78+
function cachePrimitiveColor(
79+
color: CacheableColorValue,
80+
processedColor: ?ProcessedColorValue,
81+
): void {
82+
if (primitiveColorCache.size < MAX_PRIMITIVE_COLOR_CACHE_SIZE) {
83+
primitiveColorCache.set(color, processedColor);
84+
}
85+
}
86+
5987
export default processColor;

0 commit comments

Comments
 (0)