Repository: rollup/rollup
File: src/utils/decodedSourcemap.ts (TypeScript)

Code before:
import { decode, type SourceMapMappings } from '@jridgewell/sourcemap-codec';
import type {
	ExistingDecodedSourceMap,
	ExistingRawSourceMap,
	SourceMapInput
} from '../rollup/types';

type Input = SourceMapInput | ExistingDecodedSourceMap | undefined;

export function decodedSourcemap(map: Input): ExistingDecodedSourceMap | null {
	if (!map) return null;

	if (typeof map === 'string') {
		map = JSON.parse(map) as ExistingRawSourceMap;
	}
	if (map.mappings === '') {
		return {
			mappings: [],
			names: [],
			sources: [],
			version: 3
		};
	}

	const decodedMap = {
		...(map as ExistingRawSourceMap | ExistingDecodedSourceMap),
		mappings: undefined as unknown as SourceMapMappings
	};

	const origMappings = map.mappings;
	let memoizedMappings: SourceMapMappings | undefined;

	Object.defineProperty(decodedMap, 'mappings', {
		enumerable: true,
		get() {
			if (memoizedMappings) {
				return memoizedMappings;
			}
			memoizedMappings = typeof origMappings === 'string' ? decode(origMappings) : origMappings;
			return memoizedMappings;
		},
		set(value) {
			memoizedMappings = value;
		}
	});

	return decodedMap;
}


Review comment (performance):
Memoizing the getter didn't _seem_ to have very big impact on performance from what I could tell compared to just moving the decode operation inside of a getter. That said, I did some follow-up logging and found that:

* The `decodedSourcemap()` function was called 1206 times
* The `.mappings` property was accessed 1505 times
* Only 806 of those property accesses resulted calls to `decode()`

So it definitely seems like this caching provides benefit as 699 of those property accesses would have been redundant, and only 400 calls were abandoned.

Speculating here, but is it maybe possible that creating the mappings arrays closer to the point they are used leads to them having a higher likelihood of being in an earlier GC generation as they are shorter lived?

Code after:
	}
	if (map.mappings === '') {
		return {
			mappings: [],
			names: [],
			sources: [],
			version: 3
		};
	}

	const originalMappings = map.mappings;
	const isAlreadyDecoded = typeof originalMappings !== 'string';
	const cache = {
		decodedMappings: isAlreadyDecoded ? originalMappings : undefined,
		encodedMappings: isAlreadyDecoded ? undefined : originalMappings
	};

	const decodedMap = {
		...(map as ExistingRawSourceMap | ExistingDecodedSourceMap),
		// By moving mappings behind an accessor, we can avoid unneeded computation for cases
		// where the mappings field is never actually accessed. This appears to greatly reduce
		// the overhead of sourcemap decoding in terms of both compute time and memory usage.
		get mappings() {
			if (cache.decodedMappings) {
				return cache.decodedMappings;
			}
			// If decodedMappings doesn't exist then encodedMappings should.
			// The only scenario where cache.encodedMappings should be undefined is if the map
			// this was constructed from was already decoded, or if mappings was set to a new
			// decoded string. In either case, this line shouldn't get hit.
			cache.decodedMappings = decode(cache.encodedMappings!);
			cache.encodedMappings = undefined;
			return cache.decodedMappings;
		}
	};

	sourceMapCache.set(decodedMap, cache);

	return decodedMap;
}