Skip to content

Commit

Permalink
Merge pull request #237825 from microsoft/tyriar/233990_opacity__cach…
Browse files Browse the repository at this point in the history
…e_hit

Support basic cache hits in deco style cache
  • Loading branch information
Tyriar authored Jan 13, 2025
2 parents 5bd3d12 + 4020dc6 commit 2a91911
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 159 deletions.
117 changes: 51 additions & 66 deletions src/vs/base/common/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,91 +877,76 @@ export function mapsStrictEqualIgnoreOrder(a: Map<unknown, unknown>, b: Map<unkn
}

/**
* A map that is addressable with 2 separate keys. This is useful in high performance scenarios
* where creating a composite key whenever the data is accessed is too expensive.
* A map that is addressable with an arbitrary number of keys. This is useful in high performance
* scenarios where creating a composite key whenever the data is accessed is too expensive. For
* example for a very hot function, constructing a string like `first-second-third` for every call
* will cause a significant hit to performance.
*/
export class TwoKeyMap<TFirst extends string | number, TSecond extends string | number, TValue> {
private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {};
export class NKeyMap<TValue, TKeys extends (string | boolean | number)[]> {
private _data: Map<any, any> = new Map();

public set(first: TFirst, second: TSecond, value: TValue): void {
if (!this._data[first]) {
this._data[first] = {};
}
this._data[first as string | number]![second] = value;
}

public get(first: TFirst, second: TSecond): TValue | undefined {
return this._data[first as string | number]?.[second];
}

public clear(): void {
this._data = {};
}

public *values(): IterableIterator<TValue> {
for (const first in this._data) {
for (const second in this._data[first]) {
const value = this._data[first]![second];
if (value) {
yield value;
}
/**
* Sets a value on the map. Note that unlike a standard `Map`, the first argument is the value.
* This is because the spread operator is used for the keys and must be last..
* @param value The value to set.
* @param keys The keys for the value.
*/
public set(value: TValue, ...keys: [...TKeys]): void {
let currentMap = this._data;
for (let i = 0; i < keys.length - 1; i++) {
if (!currentMap.has(keys[i])) {
currentMap.set(keys[i], new Map());
}
currentMap = currentMap.get(keys[i]);
}
currentMap.set(keys[keys.length - 1], value);
}
}

/**
* A map that is addressable with 3 separate keys. This is useful in high performance scenarios
* where creating a composite key whenever the data is accessed is too expensive.
*/
export class ThreeKeyMap<TFirst extends string | number, TSecond extends string | number, TThird extends string | number, TValue> {
private _data: { [key: string | number]: TwoKeyMap<TSecond, TThird, TValue> | undefined } = {};

public set(first: TFirst, second: TSecond, third: TThird, value: TValue): void {
if (!this._data[first]) {
this._data[first] = new TwoKeyMap();
public get(...keys: [...TKeys]): TValue | undefined {
let currentMap = this._data;
for (let i = 0; i < keys.length - 1; i++) {
if (!currentMap.has(keys[i])) {
return undefined;
}
currentMap = currentMap.get(keys[i]);
}
this._data[first as string | number]!.set(second, third, value);
}

public get(first: TFirst, second: TSecond, third: TThird): TValue | undefined {
return this._data[first as string | number]?.get(second, third);
return currentMap.get(keys[keys.length - 1]);
}

public clear(): void {
this._data = {};
this._data.clear();
}

public *values(): IterableIterator<TValue> {
for (const first in this._data) {
for (const value of this._data[first]!.values()) {
if (value) {
function* iterate(map: Map<any, any>): IterableIterator<TValue> {
for (const value of map.values()) {
if (value instanceof Map) {
yield* iterate(value);
} else {
yield value;
}
}
}
}
}

/**
* A map that is addressable with 4 separate keys. This is useful in high performance scenarios
* where creating a composite key whenever the data is accessed is too expensive.
*/
export class FourKeyMap<TFirst extends string | number, TSecond extends string | number, TThird extends string | number, TFourth extends string | number, TValue> {
private _data: TwoKeyMap<TFirst, TSecond, TwoKeyMap<TThird, TFourth, TValue>> = new TwoKeyMap();

public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void {
if (!this._data.get(first, second)) {
this._data.set(first, second, new TwoKeyMap());
}
this._data.get(first, second)!.set(third, fourth, value);
yield* iterate(this._data);
}

public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined {
return this._data.get(first, second)?.get(third, fourth);
}
/**
* Get a textual representation of the map for debugging purposes.
*/
public toString(): string {
const printMap = (map: Map<any, any>, depth: number): string => {
let result = '';
for (const [key, value] of map) {
result += `${' '.repeat(depth)}${key}: `;
if (value instanceof Map) {
result += '\n' + printMap(value, depth + 1);
} else {
result += `${value}\n`;
}
}
return result;
};

public clear(): void {
this._data.clear();
return printMap(this._data, 0);
}
}
116 changes: 38 additions & 78 deletions src/vs/base/test/common/map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import assert from 'assert';
import { BidirectionalMap, FourKeyMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, ThreeKeyMap, Touch, TwoKeyMap } from '../../common/map.js';
import { BidirectionalMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, NKeyMap, ResourceMap, SetMap, Touch } from '../../common/map.js';
import { extUriIgnorePathCase } from '../../common/resources.js';
import { URI } from '../../common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js';
Expand Down Expand Up @@ -683,96 +683,56 @@ suite('SetMap', () => {
});
});

suite('TwoKeyMap', () => {
suite('NKeyMap', () => {
ensureNoDisposablesAreLeakedInTestSuite();

test('set and get', () => {
const map = new TwoKeyMap<string, string, number>();
map.set('a', 'b', 1);
map.set('a', 'c', 2);
map.set('b', 'c', 3);
assert.strictEqual(map.get('a', 'b'), 1);
assert.strictEqual(map.get('a', 'c'), 2);
assert.strictEqual(map.get('b', 'c'), 3);
assert.strictEqual(map.get('a', 'd'), undefined);
});

test('clear', () => {
const map = new TwoKeyMap<string, string, number>();
map.set('a', 'b', 1);
map.set('a', 'c', 2);
map.set('b', 'c', 3);
map.clear();
assert.strictEqual(map.get('a', 'b'), undefined);
assert.strictEqual(map.get('a', 'c'), undefined);
assert.strictEqual(map.get('b', 'c'), undefined);
});

test('values', () => {
const map = new TwoKeyMap<string, string, number>();
map.set('a', 'b', 1);
map.set('a', 'c', 2);
map.set('b', 'c', 3);
assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]);
});
});

suite('ThreeKeyMap', () => {
ensureNoDisposablesAreLeakedInTestSuite();

test('set and get', () => {
const map = new ThreeKeyMap<string, string, string, number>();
map.set('a', 'b', 'c', 1);
map.set('a', 'c', 'd', 2);
map.set('b', 'c', 'e', 3);
assert.strictEqual(map.get('a', 'b', 'c'), 1);
assert.strictEqual(map.get('a', 'c', 'd'), 2);
assert.strictEqual(map.get('b', 'c', 'e'), 3);
assert.strictEqual(map.get('a', 'd', 'e'), undefined);
});

test('clear', () => {
const map = new ThreeKeyMap<string, string, string, number>();
map.set('a', 'b', 'c', 1);
map.set('a', 'c', 'd', 2);
map.set('b', 'c', 'e', 3);
map.clear();
assert.strictEqual(map.get('a', 'b', 'c'), undefined);
assert.strictEqual(map.get('a', 'c', 'd'), undefined);
assert.strictEqual(map.get('b', 'c', 'e'), undefined);
});

test('values', () => {
const map = new ThreeKeyMap<string, string, string, number>();
map.set('a', 'b', 'c', 1);
map.set('a', 'c', 'd', 2);
map.set('b', 'c', 'e', 3);
assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]);
});
});

suite('FourKeyMap', () => {
ensureNoDisposablesAreLeakedInTestSuite();

test('set and get', () => {
const map = new FourKeyMap<string, string, string, string, number>();
map.set('a', 'b', 'c', 'd', 1);
map.set('a', 'c', 'c', 'd', 2);
map.set('b', 'e', 'f', 'g', 3);
const map = new NKeyMap<number, [string, string, string, string]>();
map.set(1, 'a', 'b', 'c', 'd');
map.set(2, 'a', 'c', 'c', 'd');
map.set(3, 'b', 'e', 'f', 'g');
assert.strictEqual(map.get('a', 'b', 'c', 'd'), 1);
assert.strictEqual(map.get('a', 'c', 'c', 'd'), 2);
assert.strictEqual(map.get('b', 'e', 'f', 'g'), 3);
assert.strictEqual(map.get('a', 'b', 'c', 'a'), undefined);
});

test('clear', () => {
const map = new FourKeyMap<string, string, string, string, number>();
map.set('a', 'b', 'c', 'd', 1);
map.set('a', 'c', 'c', 'd', 2);
map.set('b', 'e', 'f', 'g', 3);
const map = new NKeyMap<number, [string, string, string, string]>();
map.set(1, 'a', 'b', 'c', 'd');
map.set(2, 'a', 'c', 'c', 'd');
map.set(3, 'b', 'e', 'f', 'g');
map.clear();
assert.strictEqual(map.get('a', 'b', 'c', 'd'), undefined);
assert.strictEqual(map.get('a', 'c', 'c', 'd'), undefined);
assert.strictEqual(map.get('b', 'e', 'f', 'g'), undefined);
});

test('values', () => {
const map = new NKeyMap<number, [string, string, string, string]>();
map.set(1, 'a', 'b', 'c', 'd');
map.set(2, 'a', 'c', 'c', 'd');
map.set(3, 'b', 'e', 'f', 'g');
assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]);
});

test('toString', () => {
const map = new NKeyMap<number, [string, string, string]>();
map.set(1, 'f', 'o', 'o');
map.set(2, 'b', 'a', 'r');
map.set(3, 'b', 'a', 'z');
map.set(3, 'b', 'o', 'o');
assert.strictEqual(map.toString(), [
'f: ',
' o: ',
' o: 1',
'b: ',
' a: ',
' r: 2',
' z: 3',
' o: ',
' o: 3',
'',
].join('\n'));
});
});
9 changes: 7 additions & 2 deletions src/vs/editor/browser/gpu/atlas/atlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { FourKeyMap } from '../../../../base/common/map.js';
import type { NKeyMap } from '../../../../base/common/map.js';
import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js';

/**
Expand Down Expand Up @@ -106,4 +106,9 @@ export const enum UsagePreviewColors {
Restricted = '#FF000088',
}

export type GlyphMap<T> = FourKeyMap</*chars*/string, /*tokenMetadata*/number, /*decorationStyleSetId*/number, /*rasterizerCacheKey*/string, T>;
export type GlyphMap<T> = NKeyMap<T, [
chars: string,
tokenMetadata: number,
decorationStyleSetId: number,
rasterizerCacheKey: string,
]>;
8 changes: 4 additions & 4 deletions src/vs/editor/browser/gpu/atlas/textureAtlas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { CharCode } from '../../../../base/common/charCode.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { FourKeyMap } from '../../../../base/common/map.js';
import { NKeyMap } from '../../../../base/common/map.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { MetadataConsts } from '../../../common/encodedTokenAttributes.js';
Expand Down Expand Up @@ -50,7 +50,7 @@ export class TextureAtlas extends Disposable {
* so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all
* pages with a lower index do not contain the glyph.
*/
private readonly _glyphPageIndex: GlyphMap<number> = new FourKeyMap();
private readonly _glyphPageIndex: GlyphMap<number> = new NKeyMap();

private readonly _onDidDeleteGlyphs = this._register(new Emitter<void>());
readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event;
Expand Down Expand Up @@ -126,7 +126,7 @@ export class TextureAtlas extends Disposable {
}

private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly<ITextureAtlasPageGlyph> {
this._glyphPageIndex.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, pageIndex);
this._glyphPageIndex.set(pageIndex, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
return (
this._pages[pageIndex].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)
?? (pageIndex + 1 < this._pages.length
Expand All @@ -141,7 +141,7 @@ export class TextureAtlas extends Disposable {
throw new Error(`Attempt to create a texture atlas page past the limit ${TextureAtlas.maximumPageCount}`);
}
this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType));
this._glyphPageIndex.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, this._pages.length - 1);
this._glyphPageIndex.set(this._pages.length - 1, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, tokenMetadata, decorationStyleSetId)!;
}

Expand Down
6 changes: 3 additions & 3 deletions src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { FourKeyMap } from '../../../../base/common/map.js';
import { NKeyMap } from '../../../../base/common/map.js';
import { ILogService, LogLevel } from '../../../../platform/log/common/log.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import type { IBoundingBox, IGlyphRasterizer } from '../raster/raster.js';
Expand All @@ -31,7 +31,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla
private readonly _canvas: OffscreenCanvas;
get source(): OffscreenCanvas { return this._canvas; }

private readonly _glyphMap: GlyphMap<ITextureAtlasPageGlyph> = new FourKeyMap();
private readonly _glyphMap: GlyphMap<ITextureAtlasPageGlyph> = new NKeyMap();
private readonly _glyphInOrderSet: Set<ITextureAtlasPageGlyph> = new Set();
get glyphs(): IterableIterator<ITextureAtlasPageGlyph> {
return this._glyphInOrderSet.values();
Expand Down Expand Up @@ -89,7 +89,7 @@ export class TextureAtlasPage extends Disposable implements IReadableTextureAtla
}

// Save the glyph
this._glyphMap.set(chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey, glyph);
this._glyphMap.set(glyph, chars, tokenMetadata, decorationStyleSetId, rasterizer.cacheKey);
this._glyphInOrderSet.add(glyph);

// Update page version and it's tracked used area
Expand Down
6 changes: 3 additions & 3 deletions src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { getActiveWindow } from '../../../../base/browser/dom.js';
import { BugIndicatingError } from '../../../../base/common/errors.js';
import { TwoKeyMap } from '../../../../base/common/map.js';
import { NKeyMap } from '../../../../base/common/map.js';
import { ensureNonNullable } from '../gpuUtils.js';
import type { IRasterizedGlyph } from '../raster/raster.js';
import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js';
Expand All @@ -29,7 +29,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator {
private readonly _ctx: OffscreenCanvasRenderingContext2D;

private readonly _slabs: ITextureAtlasSlab[] = [];
private readonly _activeSlabsByDims: TwoKeyMap<number, number, ITextureAtlasSlab> = new TwoKeyMap();
private readonly _activeSlabsByDims: NKeyMap<ITextureAtlasSlab, [number, number]> = new NKeyMap();

private readonly _unusedRects: ITextureAtlasSlabUnusedRect[] = [];

Expand Down Expand Up @@ -243,7 +243,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator {
});
}
this._slabs.push(slab);
this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab);
this._activeSlabsByDims.set(slab, desiredSlabSize.w, desiredSlabSize.h);
}

const glyphsPerRow = Math.floor(this._slabW / slab.entryW);
Expand Down
Loading

0 comments on commit 2a91911

Please sign in to comment.