@@ -8,22 +8,34 @@ import { Buffer } from 'common/buffer/Buffer';
88import { CircularList } from 'common/CircularList' ;
99import { MockOptionsService , MockBufferService , MockLogService , createCellData } from 'common/TestUtils.test' ;
1010import { BufferLine , DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine' ;
11+ import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache' ;
1112import { CellData } from 'common/buffer/CellData' ;
1213import { ExtendedAttrs } from 'common/buffer/AttributeData' ;
1314
1415const INIT_COLS = 80 ;
1516const INIT_ROWS = 24 ;
1617const INIT_SCROLLBACK = 1000 ;
18+ const TEST_STRING_CACHE = new BufferLineStringCache ( ) ;
19+
20+ class TestBuffer extends Buffer {
21+ public getStringCache ( ) : BufferLineStringCache {
22+ return ( this as unknown as { _stringCache : BufferLineStringCache } ) . _stringCache ;
23+ }
24+
25+ public getStringCacheClearTimeout ( ) : unknown {
26+ return ( this . getStringCache ( ) as unknown as { _clearTimeout : { value : unknown } } ) . _clearTimeout . value ;
27+ }
28+ }
1729
1830describe ( 'Buffer' , ( ) => {
1931 let optionsService : MockOptionsService ;
2032 let bufferService : MockBufferService ;
21- let buffer : Buffer ;
33+ let buffer : TestBuffer ;
2234
2335 beforeEach ( ( ) => {
2436 optionsService = new MockOptionsService ( { scrollback : INIT_SCROLLBACK } ) ;
2537 bufferService = new MockBufferService ( INIT_COLS , INIT_ROWS ) ;
26- buffer = new Buffer ( true , optionsService , bufferService , new MockLogService ( ) ) ;
38+ buffer = new TestBuffer ( true , optionsService , bufferService , new MockLogService ( ) ) ;
2739 } ) ;
2840
2941 describe ( 'constructor' , ( ) => {
@@ -151,7 +163,7 @@ describe('Buffer', () => {
151163
152164 describe ( 'no scrollback' , ( ) => {
153165 it ( 'should trim from the top of the buffer when the cursor reaches the bottom' , ( ) => {
154- buffer = new Buffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
166+ buffer = new TestBuffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
155167 assert . equal ( buffer . lines . maxLength , INIT_ROWS ) ;
156168 buffer . y = INIT_ROWS - 1 ;
157169 buffer . fillViewportRows ( ) ;
@@ -1054,7 +1066,7 @@ describe('Buffer', () => {
10541066 describe ( 'buffer marked to have no scrollback' , ( ) => {
10551067 it ( 'should always have a scrollback of 0' , ( ) => {
10561068 // Test size on initialization
1057- buffer = new Buffer ( false , new MockOptionsService ( { scrollback : 1000 } ) , bufferService , new MockLogService ( ) ) ;
1069+ buffer = new TestBuffer ( false , new MockOptionsService ( { scrollback : 1000 } ) , bufferService , new MockLogService ( ) ) ;
10581070 buffer . fillViewportRows ( ) ;
10591071 assert . equal ( buffer . lines . maxLength , INIT_ROWS ) ;
10601072 // Test size on buffer increase
@@ -1068,15 +1080,15 @@ describe('Buffer', () => {
10681080
10691081 describe ( 'addMarker' , ( ) => {
10701082 it ( 'should adjust a marker line when the buffer is trimmed' , ( ) => {
1071- buffer = new Buffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
1083+ buffer = new TestBuffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
10721084 buffer . fillViewportRows ( ) ;
10731085 const marker = buffer . addMarker ( buffer . lines . length - 1 ) ;
10741086 assert . equal ( marker . line , buffer . lines . length - 1 ) ;
10751087 buffer . lines . onTrimEmitter . fire ( 1 ) ;
10761088 assert . equal ( marker . line , buffer . lines . length - 2 ) ;
10771089 } ) ;
10781090 it ( 'should dispose of a marker if it is trimmed off the buffer' , ( ) => {
1079- buffer = new Buffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
1091+ buffer = new TestBuffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
10801092 buffer . fillViewportRows ( ) ;
10811093 assert . equal ( buffer . markers . length , 0 ) ;
10821094 const marker = buffer . addMarker ( 0 ) ;
@@ -1088,7 +1100,7 @@ describe('Buffer', () => {
10881100 } ) ;
10891101 it ( 'should call onDispose' , ( ) => {
10901102 const eventStack : string [ ] = [ ] ;
1091- buffer = new Buffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
1103+ buffer = new TestBuffer ( true , new MockOptionsService ( { scrollback : 0 } ) , bufferService , new MockLogService ( ) ) ;
10921104 buffer . fillViewportRows ( ) ;
10931105 assert . equal ( buffer . markers . length , 0 ) ;
10941106 const marker = buffer . addMarker ( 0 ) ;
@@ -1104,7 +1116,7 @@ describe('Buffer', () => {
11041116
11051117 describe ( 'translateBufferLineToString' , ( ) => {
11061118 it ( 'should handle selecting a section of ascii text' , ( ) => {
1107- const line = new BufferLine ( 4 ) ;
1119+ const line = new BufferLine ( TEST_STRING_CACHE , 4 ) ;
11081120 line . setCell ( 0 , createCellData ( 0 , 'a' , 1 ) ) ;
11091121 line . setCell ( 1 , createCellData ( 0 , 'b' , 1 ) ) ;
11101122 line . setCell ( 2 , createCellData ( 0 , 'c' , 1 ) ) ;
@@ -1116,7 +1128,7 @@ describe('Buffer', () => {
11161128 } ) ;
11171129
11181130 it ( 'should handle a cut-off double width character by including it' , ( ) => {
1119- const line = new BufferLine ( 3 ) ;
1131+ const line = new BufferLine ( TEST_STRING_CACHE , 3 ) ;
11201132 line . setCell ( 0 , createCellData ( 0 , '語' , 2 ) ) ;
11211133 line . setCell ( 1 , createCellData ( 0 , '' , 0 ) ) ;
11221134 line . setCell ( 2 , createCellData ( 0 , 'a' , 1 ) ) ;
@@ -1127,7 +1139,7 @@ describe('Buffer', () => {
11271139 } ) ;
11281140
11291141 it ( 'should handle a zero width character in the middle of the string by not including it' , ( ) => {
1130- const line = new BufferLine ( 3 ) ;
1142+ const line = new BufferLine ( TEST_STRING_CACHE , 3 ) ;
11311143 line . setCell ( 0 , createCellData ( 0 , '語' , 2 ) ) ;
11321144 line . setCell ( 1 , createCellData ( 0 , '' , 0 ) ) ;
11331145 line . setCell ( 2 , createCellData ( 0 , 'a' , 1 ) ) ;
@@ -1144,7 +1156,7 @@ describe('Buffer', () => {
11441156 } ) ;
11451157
11461158 it ( 'should handle single width emojis' , ( ) => {
1147- const line = new BufferLine ( 2 ) ;
1159+ const line = new BufferLine ( TEST_STRING_CACHE , 2 ) ;
11481160 line . setCell ( 0 , createCellData ( 0 , '😁' , 1 ) ) ;
11491161 line . setCell ( 1 , createCellData ( 0 , 'a' , 1 ) ) ;
11501162 buffer . lines . set ( 0 , line ) ;
@@ -1157,7 +1169,7 @@ describe('Buffer', () => {
11571169 } ) ;
11581170
11591171 it ( 'should handle double width emojis' , ( ) => {
1160- const line = new BufferLine ( 2 ) ;
1172+ const line = new BufferLine ( TEST_STRING_CACHE , 2 ) ;
11611173 line . setCell ( 0 , createCellData ( 0 , '😁' , 2 ) ) ;
11621174 line . setCell ( 1 , createCellData ( 0 , '' , 0 ) ) ;
11631175 buffer . lines . set ( 0 , line ) ;
@@ -1168,7 +1180,7 @@ describe('Buffer', () => {
11681180 const str2 = buffer . translateBufferLineToString ( 0 , true , 0 , 2 ) ;
11691181 assert . equal ( str2 , '😁' ) ;
11701182
1171- const line2 = new BufferLine ( 3 ) ;
1183+ const line2 = new BufferLine ( TEST_STRING_CACHE , 3 ) ;
11721184 line2 . setCell ( 0 , createCellData ( 0 , '😁' , 2 ) ) ;
11731185 line2 . setCell ( 1 , createCellData ( 0 , '' , 0 ) ) ;
11741186 line2 . setCell ( 2 , createCellData ( 0 , 'a' , 1 ) ) ;
@@ -1179,6 +1191,99 @@ describe('Buffer', () => {
11791191 } ) ;
11801192 } ) ;
11811193
1194+ describe ( 'line string cache cleanup' , ( ) => {
1195+ it ( 'should clear shared cache entries with a single timer' , ( ) => {
1196+ const originalSetTimeout = globalThis . setTimeout ;
1197+ const originalClearTimeout = globalThis . clearTimeout ;
1198+ const originalDateNow = Date . now ;
1199+ let timeoutId = 0 ;
1200+ let now = 0 ;
1201+ const clearedTimeouts : number [ ] = [ ] ;
1202+ const scheduledTimeouts = new Map < number , { delay : number , fire : ( ) => void } > ( ) ;
1203+ ( globalThis as any ) . setTimeout = ( ( handler : ( ...args : any [ ] ) => void , timeout ?: number ) => {
1204+ const id = ++ timeoutId ;
1205+ scheduledTimeouts . set ( id , {
1206+ delay : timeout ?? 0 ,
1207+ fire : ( ) => {
1208+ scheduledTimeouts . delete ( id ) ;
1209+ handler ( ) ;
1210+ }
1211+ } ) ;
1212+ return id as ReturnType < typeof setTimeout > ;
1213+ } ) as typeof setTimeout ;
1214+ ( globalThis as any ) . clearTimeout = ( ( id : ReturnType < typeof setTimeout > ) => {
1215+ const numericId = id as unknown as number ;
1216+ clearedTimeouts . push ( numericId ) ;
1217+ scheduledTimeouts . delete ( numericId ) ;
1218+ } ) as typeof clearTimeout ;
1219+ Date . now = ( ) => now ;
1220+ try {
1221+ buffer . fillViewportRows ( ) ;
1222+ buffer . lines . get ( 0 ) ! . setCell ( 0 , createCellData ( 0 , 'a' , 1 ) ) ;
1223+ buffer . lines . get ( 1 ) ! . setCell ( 0 , createCellData ( 0 , 'b' , 1 ) ) ;
1224+
1225+ assert . equal ( buffer . translateBufferLineToString ( 0 , false ) , `a${ ' ' . repeat ( INIT_COLS - 1 ) } ` ) ;
1226+ assert . equal ( buffer . translateBufferLineToString ( 1 , false ) , `b${ ' ' . repeat ( INIT_COLS - 1 ) } ` ) ;
1227+
1228+ const cache = buffer . getStringCache ( ) ;
1229+ assert . equal ( cache . entries . size , 2 ) ;
1230+ assert . ok ( buffer . getStringCacheClearTimeout ( ) !== undefined ) ;
1231+ assert . equal ( scheduledTimeouts . size , 1 ) ;
1232+ assert . equal ( [ ...scheduledTimeouts . values ( ) ] [ 0 ] . delay , 15000 ) ;
1233+ const initialTimerCreationCount = timeoutId ;
1234+
1235+ now = 5000 ;
1236+ assert . equal ( buffer . translateBufferLineToString ( 0 , false ) , `a${ ' ' . repeat ( INIT_COLS - 1 ) } ` ) ;
1237+ assert . equal ( timeoutId , initialTimerCreationCount ) ;
1238+ assert . equal ( scheduledTimeouts . size , 1 ) ;
1239+ assert . deepEqual ( clearedTimeouts , [ ] ) ;
1240+
1241+ now = 15000 ;
1242+ [ ...scheduledTimeouts . values ( ) ] [ 0 ] . fire ( ) ;
1243+ assert . equal ( timeoutId , initialTimerCreationCount + 1 ) ;
1244+ assert . ok ( buffer . getStringCacheClearTimeout ( ) !== undefined ) ;
1245+ assert . equal ( scheduledTimeouts . size , 1 ) ;
1246+ assert . equal ( [ ...scheduledTimeouts . values ( ) ] [ 0 ] . delay , 5000 ) ;
1247+
1248+ now = 20000 ;
1249+ [ ...scheduledTimeouts . values ( ) ] [ 0 ] . fire ( ) ;
1250+
1251+ assert . equal ( cache . entries . size , 0 ) ;
1252+ assert . equal ( buffer . getStringCacheClearTimeout ( ) , undefined ) ;
1253+
1254+ assert . equal ( buffer . translateBufferLineToString ( 0 , false ) , `a${ ' ' . repeat ( INIT_COLS - 1 ) } ` ) ;
1255+ assert . equal ( cache . entries . size , 1 ) ;
1256+ } finally {
1257+ Date . now = originalDateNow ;
1258+ globalThis . setTimeout = originalSetTimeout ;
1259+ globalThis . clearTimeout = originalClearTimeout ;
1260+ }
1261+ } ) ;
1262+
1263+ it ( 'should reset line string cache state on clear and resize' , ( ) => {
1264+ buffer . fillViewportRows ( ) ;
1265+ buffer . lines . get ( 0 ) ! . setCell ( 0 , createCellData ( 0 , 'a' , 1 ) ) ;
1266+ buffer . translateBufferLineToString ( 0 , false ) ;
1267+
1268+ const cache = buffer . getStringCache ( ) ;
1269+ assert . equal ( cache . entries . size , 1 ) ;
1270+ assert . ok ( buffer . getStringCacheClearTimeout ( ) !== undefined ) ;
1271+
1272+ buffer . clear ( ) ;
1273+ assert . equal ( cache . entries . size , 0 ) ;
1274+ assert . equal ( buffer . getStringCacheClearTimeout ( ) , undefined ) ;
1275+
1276+ buffer . fillViewportRows ( ) ;
1277+ buffer . lines . get ( 0 ) ! . setCell ( 0 , createCellData ( 0 , 'b' , 1 ) ) ;
1278+ buffer . translateBufferLineToString ( 0 , false ) ;
1279+ assert . equal ( cache . entries . size , 1 ) ;
1280+
1281+ buffer . resize ( INIT_COLS - 1 , INIT_ROWS ) ;
1282+ assert . equal ( cache . entries . size , 0 ) ;
1283+ assert . equal ( buffer . getStringCacheClearTimeout ( ) , undefined ) ;
1284+ } ) ;
1285+ } ) ;
1286+
11821287 describe ( 'memory cleanup after shrinking' , ( ) => {
11831288 it ( 'should realign memory from idle task execution' , async ( ) => {
11841289 buffer . fillViewportRows ( ) ;
0 commit comments