Skip to content

Commit 5e38946

Browse files
jasnelladuh95
authored andcommitted
quic: add aliased struct arenas
Every stream, session, and endpoint creates aliased structs for stats and state. These were creating v8::ArrayBuffer allocations and views for each instance, which is expensive. This adds a new arena mechanism for AliasedStructs that allocates in pages and allows Streams and Sessions to share the same underlying ArrayBuffer for their stats and state. Since these are never exposed to users, this is safe and results in a significant reduction in allocation counts. Each arena maintains a freelist of pages, where each page is a max of 16KB bytes. Pages are lazily allocated and freed as needed. Each slot in the arena corresponds to a single struct instance, and the slot index is used to calculate the byte offset within the page for that struct's view. The perf improvement is modest but measurable. The key benefit is in reduced memory fragmentation and GC overhead. Signed-off-by: James M Snell <jasnell@gmail.com> Assisted-by: Opencode:Opus 4.6 PR-URL: #63267 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 9543043 commit 5e38946

9 files changed

Lines changed: 744 additions & 245 deletions

File tree

‎lib/internal/quic/quic.js‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,8 +1472,10 @@ class QuicStream {
14721472
this.#handle[kOwner]=this;
14731473
this.#session =session;
14741474
this.#direction =direction;
1475-
this.#stats =newQuicStreamStats(kPrivateConstructor,this.#handle.stats);
1476-
this.#state =newQuicStreamState(kPrivateConstructor,this.#handle.state);
1475+
this.#stats =newQuicStreamStats(
1476+
kPrivateConstructor,this.#handle.stats,this.#handle.statsByteOffset);
1477+
this.#state =newQuicStreamState(
1478+
kPrivateConstructor,this.#handle.state,this.#handle.stateByteOffset);
14771479
this.#reader =this.#handle.getReader();
14781480

14791481
if(hasObserver('quic')){
@@ -2656,8 +2658,10 @@ class QuicSession {
26562658
this.#pendingQlog =handle._pendingQlog;
26572659
handle._pendingQlog=undefined;
26582660
}
2659-
this.#stats =newQuicSessionStats(kPrivateConstructor,handle.stats);
2660-
this.#state =newQuicSessionState(kPrivateConstructor,handle.state);
2661+
this.#stats =newQuicSessionStats(
2662+
kPrivateConstructor,handle.stats,handle.statsByteOffset);
2663+
this.#state =newQuicSessionState(
2664+
kPrivateConstructor,handle.state,handle.stateByteOffset);
26612665

26622666
if(hasObserver('quic')){
26632667
startPerf(this,kPerfEntry,{type: 'quic',name: 'QuicSession'});

‎lib/internal/quic/state.js‎

Lines changed: 67 additions & 53 deletions
Large diffs are not rendered by default.

‎lib/internal/quic/stats.js‎

Lines changed: 78 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -332,163 +332,168 @@ class QuicSessionStats {
332332
* @param {symbol} privateSymbol
333333
* @param {ArrayBuffer} buffer
334334
*/
335-
constructor(privateSymbol,buffer){
335+
/** @type {number} */
336+
#offset =0;
337+
338+
constructor(privateSymbol,view,byteOffset=0){
336339
// We use the kPrivateConstructor symbol to restrict the ability to
337340
// create new instances of QuicSessionStats to internal code.
338341
if(privateSymbol!==kPrivateConstructor){
339342
thrownewERR_ILLEGAL_CONSTRUCTOR();
340343
}
341-
if(!isArrayBuffer(buffer)){
342-
thrownewERR_INVALID_ARG_TYPE('buffer',['ArrayBuffer'],buffer);
344+
if(isArrayBuffer(view)){
345+
this.#handle =newBigUint64Array(view);
346+
}else{
347+
this.#handle =view;
343348
}
344-
this.#handle=newBigUint64Array(buffer);
349+
this.#offset=byteOffset/8;
345350
}
346351

347352
/** @type {bigint} */
348353
getcreatedAt(){
349-
returnthis.#handle[IDX_STATS_SESSION_CREATED_AT];
354+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_CREATED_AT];
350355
}
351356

352357
/** @type {bigint} */
353358
getdestroyedAt(){
354-
returnthis.#handle[IDX_STATS_SESSION_DESTROYED_AT];
359+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_DESTROYED_AT];
355360
}
356361

357362
/** @type {bigint} */
358363
getclosingAt(){
359-
returnthis.#handle[IDX_STATS_SESSION_CLOSING_AT];
364+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_CLOSING_AT];
360365
}
361366

362367
/** @type {bigint} */
363368
gethandshakeCompletedAt(){
364-
returnthis.#handle[IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT];
369+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT];
365370
}
366371

367372
/** @type {bigint} */
368373
gethandshakeConfirmedAt(){
369-
returnthis.#handle[IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT];
374+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT];
370375
}
371376

372377
/** @type {bigint} */
373378
getbytesReceived(){
374-
returnthis.#handle[IDX_STATS_SESSION_BYTES_RECEIVED];
379+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BYTES_RECEIVED];
375380
}
376381

377382
/** @type {bigint} */
378383
getbidiInStreamCount(){
379-
returnthis.#handle[IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT];
384+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT];
380385
}
381386

382387
/** @type {bigint} */
383388
getbidiOutStreamCount(){
384-
returnthis.#handle[IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT];
389+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT];
385390
}
386391

387392
/** @type {bigint} */
388393
getuniInStreamCount(){
389-
returnthis.#handle[IDX_STATS_SESSION_UNI_IN_STREAM_COUNT];
394+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_UNI_IN_STREAM_COUNT];
390395
}
391396

392397
/** @type {bigint} */
393398
getuniOutStreamCount(){
394-
returnthis.#handle[IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT];
399+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT];
395400
}
396401

397402
/** @type {bigint} */
398403
getmaxBytesInFlight(){
399-
returnthis.#handle[IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
404+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
400405
}
401406

402407
/** @type {bigint} */
403408
getbytesInFlight(){
404-
returnthis.#handle[IDX_STATS_SESSION_BYTES_IN_FLIGHT];
409+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BYTES_IN_FLIGHT];
405410
}
406411

407412
/** @type {bigint} */
408413
getblockCount(){
409-
returnthis.#handle[IDX_STATS_SESSION_BLOCK_COUNT];
414+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BLOCK_COUNT];
410415
}
411416

412417
/** @type {bigint} */
413418
getcwnd(){
414-
returnthis.#handle[IDX_STATS_SESSION_CWND];
419+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_CWND];
415420
}
416421

417422
/** @type {bigint} */
418423
getlatestRtt(){
419-
returnthis.#handle[IDX_STATS_SESSION_LATEST_RTT];
424+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_LATEST_RTT];
420425
}
421426

422427
/** @type {bigint} */
423428
getminRtt(){
424-
returnthis.#handle[IDX_STATS_SESSION_MIN_RTT];
429+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_MIN_RTT];
425430
}
426431

427432
/** @type {bigint} */
428433
getrttVar(){
429-
returnthis.#handle[IDX_STATS_SESSION_RTTVAR];
434+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_RTTVAR];
430435
}
431436

432437
/** @type {bigint} */
433438
getsmoothedRtt(){
434-
returnthis.#handle[IDX_STATS_SESSION_SMOOTHED_RTT];
439+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_SMOOTHED_RTT];
435440
}
436441

437442
/** @type {bigint} */
438443
getssthresh(){
439-
returnthis.#handle[IDX_STATS_SESSION_SSTHRESH];
444+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_SSTHRESH];
440445
}
441446

442447
getpktSent(){
443-
returnthis.#handle[IDX_STATS_SESSION_PKT_SENT];
448+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_PKT_SENT];
444449
}
445450

446451
getbytesSent(){
447-
returnthis.#handle[IDX_STATS_SESSION_BYTES_SENT];
452+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BYTES_SENT];
448453
}
449454

450455
getpktRecv(){
451-
returnthis.#handle[IDX_STATS_SESSION_PKT_RECV];
456+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_PKT_RECV];
452457
}
453458

454459
getbytesRecv(){
455-
returnthis.#handle[IDX_STATS_SESSION_BYTES_RECV];
460+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BYTES_RECV];
456461
}
457462

458463
getpktLost(){
459-
returnthis.#handle[IDX_STATS_SESSION_PKT_LOST];
464+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_PKT_LOST];
460465
}
461466

462467
getbytesLost(){
463-
returnthis.#handle[IDX_STATS_SESSION_BYTES_LOST];
468+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_BYTES_LOST];
464469
}
465470

466471
getpingRecv(){
467-
returnthis.#handle[IDX_STATS_SESSION_PING_RECV];
472+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_PING_RECV];
468473
}
469474

470475
getpktDiscarded(){
471-
returnthis.#handle[IDX_STATS_SESSION_PKT_DISCARDED];
476+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_PKT_DISCARDED];
472477
}
473478

474479
/** @type {bigint} */
475480
getdatagramsReceived(){
476-
returnthis.#handle[IDX_STATS_SESSION_DATAGRAMS_RECEIVED];
481+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_DATAGRAMS_RECEIVED];
477482
}
478483

479484
/** @type {bigint} */
480485
getdatagramsSent(){
481-
returnthis.#handle[IDX_STATS_SESSION_DATAGRAMS_SENT];
486+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_DATAGRAMS_SENT];
482487
}
483488

484489
/** @type {bigint} */
485490
getdatagramsAcknowledged(){
486-
returnthis.#handle[IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED];
491+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED];
487492
}
488493

489494
/** @type {bigint} */
490495
getdatagramsLost(){
491-
returnthis.#handle[IDX_STATS_SESSION_DATAGRAMS_LOST];
496+
returnthis.#handle[this.#offset +IDX_STATS_SESSION_DATAGRAMS_LOST];
492497
}
493498

494499
toString(){
@@ -590,88 +595,99 @@ class QuicSessionStats {
590595
}
591596

592597
[kFinishClose](){
593-
// Snapshot the stats into a new BigUint64Array since the underlying
594-
// buffer will be destroyed.
595-
this.#handle =newBigUint64Array(this.#handle);
598+
// Snapshot this session's stats slice into a standalone BigUint64Array.
599+
constcount=IDX_STATS_SESSION_DATAGRAMS_LOST+1;
600+
constsnapshot=newBigUint64Array(count);
601+
for(leti=0;i<count;i++){
602+
snapshot[i]=this.#handle[this.#offset +i];
603+
}
604+
this.#handle =snapshot;
605+
this.#offset =0;
596606
this.#disconnected =true;
597607
}
598608
}
599609

600610
classQuicStreamStats{
601611
/** @type {BigUint64Array} */
602612
#handle;
613+
/** @type {number} */
614+
#offset =0;
603615
/** type {boolean} */
604616
#disconnected =false;
605617

606618
/**
607619
* @param {symbol} privateSymbol
608-
* @param {ArrayBuffer} buffer
620+
* @param {BigUint64Array|ArrayBuffer} view
621+
* @param {number} [byteOffset] - byte offset into the shared page view
609622
*/
610-
constructor(privateSymbol,buffer){
623+
constructor(privateSymbol,view,byteOffset=0){
611624
// We use the kPrivateConstructor symbol to restrict the ability to
612625
// create new instances of QuicStreamStats to internal code.
613626
if(privateSymbol!==kPrivateConstructor){
614627
thrownewERR_ILLEGAL_CONSTRUCTOR();
615628
}
616-
if(!isArrayBuffer(buffer)){
617-
thrownewERR_INVALID_ARG_TYPE('buffer',['ArrayBuffer'],buffer);
629+
if(isArrayBuffer(view)){
630+
this.#handle =newBigUint64Array(view);
631+
}else{
632+
this.#handle =view;
618633
}
619-
this.#handle =newBigUint64Array(buffer);
634+
// Convert byte offset to element offset (8 bytes per uint64).
635+
this.#offset =byteOffset/8;
620636
}
621637

622638
/** @type {bigint} */
623639
getcreatedAt(){
624-
returnthis.#handle[IDX_STATS_STREAM_CREATED_AT];
640+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_CREATED_AT];
625641
}
626642

627643
/** @type {bigint} */
628644
getopenedAt(){
629-
returnthis.#handle[IDX_STATS_STREAM_OPENED_AT];
645+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_OPENED_AT];
630646
}
631647

632648
/** @type {bigint} */
633649
getreceivedAt(){
634-
returnthis.#handle[IDX_STATS_STREAM_RECEIVED_AT];
650+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_RECEIVED_AT];
635651
}
636652

637653
/** @type {bigint} */
638654
getackedAt(){
639-
returnthis.#handle[IDX_STATS_STREAM_ACKED_AT];
655+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_ACKED_AT];
640656
}
641657

642658
/** @type {bigint} */
643659
getdestroyedAt(){
644-
returnthis.#handle[IDX_STATS_STREAM_DESTROYED_AT];
660+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_DESTROYED_AT];
645661
}
646662

647663
/** @type {bigint} */
648664
getbytesReceived(){
649-
returnthis.#handle[IDX_STATS_STREAM_BYTES_RECEIVED];
665+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_BYTES_RECEIVED];
650666
}
651667

652668
/** @type {bigint} */
653669
getbytesSent(){
654-
returnthis.#handle[IDX_STATS_STREAM_BYTES_SENT];
670+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_BYTES_SENT];
655671
}
656672

657673
/** @type {bigint} */
658674
getmaxOffset(){
659-
returnthis.#handle[IDX_STATS_STREAM_MAX_OFFSET];
675+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_MAX_OFFSET];
660676
}
661677

662678
/** @type {bigint} */
663679
getmaxOffsetAcknowledged(){
664-
returnthis.#handle[IDX_STATS_STREAM_MAX_OFFSET_ACK];
680+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_MAX_OFFSET_ACK];
665681
}
666682

667683
/** @type {bigint} */
668684
getmaxOffsetReceived(){
669-
returnthis.#handle[IDX_STATS_STREAM_MAX_OFFSET_RECV];
685+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_MAX_OFFSET_RECV];
670686
}
671687

672688
/** @type {bigint} */
673689
getfinalSize(){
674-
returnthis.#handle[IDX_STATS_STREAM_FINAL_SIZE];
690+
returnthis.#handle[this.#offset +IDX_STATS_STREAM_FINAL_SIZE];
675691
}
676692

677693
toString(){
@@ -735,9 +751,14 @@ class QuicStreamStats {
735751
}
736752

737753
[kFinishClose](){
738-
// Snapshot the stats into a new BigUint64Array since the underlying
739-
// buffer will be destroyed.
740-
this.#handle =newBigUint64Array(this.#handle);
754+
// Snapshot this stream's stats slice into a standalone BigUint64Array.
755+
constcount=IDX_STATS_STREAM_FINAL_SIZE+1;
756+
constsnapshot=newBigUint64Array(count);
757+
for(leti=0;i<count;i++){
758+
snapshot[i]=this.#handle[this.#offset +i];
759+
}
760+
this.#handle =snapshot;
761+
this.#offset =0;
741762
this.#disconnected =true;
742763
}
743764
}

0 commit comments

Comments
 (0)