diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index bc06aafaec232..c8f9de67964e2 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -877,91 +877,76 @@ export function mapsStrictEqualIgnoreOrder(a: Map, b: Map { - private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; +export class NKeyMap { + private _data: Map = 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 { - 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 { - private _data: { [key: string | number]: TwoKeyMap | 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 { - for (const first in this._data) { - for (const value of this._data[first]!.values()) { - if (value) { + function* iterate(map: Map): IterableIterator { + 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 { - private _data: TwoKeyMap> = 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, 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); } } diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index aa42402388556..895726ab312d4 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -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'; @@ -683,82 +683,14 @@ suite('SetMap', () => { }); }); -suite('TwoKeyMap', () => { +suite('NKeyMap', () => { ensureNoDisposablesAreLeakedInTestSuite(); test('set and get', () => { - const map = new TwoKeyMap(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); - 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(); + 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); @@ -766,13 +698,41 @@ suite('FourKeyMap', () => { }); test('clear', () => { - const map = new FourKeyMap(); - 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(); + 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(); + 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(); + 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')); + }); }); diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts index 42d1eacaa8910..17900d5273c88 100644 --- a/src/vs/editor/browser/gpu/atlas/atlas.ts +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -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'; /** @@ -106,4 +106,9 @@ export const enum UsagePreviewColors { Restricted = '#FF000088', } -export type GlyphMap = FourKeyMap; +export type GlyphMap = NKeyMap; diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts index 472f39e67098e..32987628de34e 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -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'; @@ -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 = new FourKeyMap(); + private readonly _glyphPageIndex: GlyphMap = new NKeyMap(); private readonly _onDidDeleteGlyphs = this._register(new Emitter()); readonly onDidDeleteGlyphs = this._onDidDeleteGlyphs.event; @@ -126,7 +126,7 @@ export class TextureAtlas extends Disposable { } private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, tokenMetadata: number, decorationStyleSetId: number): Readonly { - 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 @@ -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)!; } diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts index 4c48a3f70e229..1548f772e88bd 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -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'; @@ -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 = new FourKeyMap(); + private readonly _glyphMap: GlyphMap = new NKeyMap(); private readonly _glyphInOrderSet: Set = new Set(); get glyphs(): IterableIterator { return this._glyphInOrderSet.values(); @@ -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 diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts index f9fa20dcfdb2d..e6340cac982da 100644 --- a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -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'; @@ -29,7 +29,7 @@ export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { private readonly _ctx: OffscreenCanvasRenderingContext2D; private readonly _slabs: ITextureAtlasSlab[] = []; - private readonly _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + private readonly _activeSlabsByDims: NKeyMap = new NKeyMap(); private readonly _unusedRects: ITextureAtlasSlabUnusedRect[] = []; @@ -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); diff --git a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts index 879b72470f14b..1b1c07df16303 100644 --- a/src/vs/editor/browser/gpu/css/decorationStyleCache.ts +++ b/src/vs/editor/browser/gpu/css/decorationStyleCache.ts @@ -3,6 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { NKeyMap } from '../../../../base/common/map.js'; + export interface IDecorationStyleSet { /** * A 24-bit number representing `color`. @@ -29,7 +31,8 @@ export class DecorationStyleCache { private _nextId = 1; - private readonly _cache = new Map(); + private readonly _cacheById = new Map(); + private readonly _cacheByStyle = new NKeyMap(); getOrCreateEntry( color: number | undefined, @@ -39,6 +42,10 @@ export class DecorationStyleCache { if (color === undefined && bold === undefined && opacity === undefined) { return 0; } + const result = this._cacheByStyle.get(color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); + if (result) { + return result.id; + } const id = this._nextId++; const entry = { id, @@ -46,7 +53,8 @@ export class DecorationStyleCache { bold, opacity, }; - this._cache.set(id, entry); + this._cacheById.set(id, entry); + this._cacheByStyle.set(entry, color ?? 0, bold ? 1 : 0, opacity === undefined ? '' : opacity.toFixed(2)); return id; } @@ -54,6 +62,6 @@ export class DecorationStyleCache { if (id === 0) { return undefined; } - return this._cache.get(id); + return this._cacheById.get(id); } }