import { strict as assert } from "assert" ; import { Random } from "best-random" ; import { IChannelServices } from "@fluidframework/datastore-definitions" ; import { MockFluidDataStoreRuntime , MockStorage , MockContainerRuntimeFactoryForReconnection , MockContainerRuntimeForReconnection , } from "@fluidframework/test-runtime-utils" ; import { SharedMatrix , SharedMatrixFactory } from ".." ; import { extract , expectSize } from "./utils" ; describe ( "Matrix" , ( ) => { describe ( "stress" , ( ) => { let containerRuntimeFactory ; let matrices ; let runtimes = [ ] ; let trace ; /**
         * Drains the queue of pending ops for each client and vets that all matrices converged on the same state.
         */ const expect = async ( ) => { for ( let matrixIndex = 0 ; matrixIndex < runtimes . length ; matrixIndex ++ ) { const runtime = runtimes [ matrixIndex ] ; if ( ! runtime . connected ) { trace ?. push ( `containerRuntime${ matrixIndex + 1 }.connected = true;` ) ; runtime . connected = true ; } } trace ?. push ( "await expect();" ) ; containerRuntimeFactory . processAllMessages ( ) ; const matrix0 = matrices [ 0 ] ; const actual0 = extract ( matrix0 ) ; for ( let i = 1 ; i < matrices . length ; i ++ ) { const matrixN = matrices [ i ] ; const actualN = extract ( matrixN ) ; assert . deepEqual ( actual0 , actualN ) ; expectSize ( matrixN , matrix0 . rowCount , matrix0 . colCount ) ; } } ; /**
         * Performs a stress run using the given parameters.
         *
         * 'syncProbability' is the probability that the clients will drain their queue of incoming messages
         * and check for convergence.
         *
         * 'disconnectProbability' is the probability that a client will disconnect, forcing it to regenerate
         * and resubmit any pending local operations on the next sync.
         *
         * 'seed' is the 32-bit integer used to seed the PRNG.
         */ async function stress ( numClients , numOps , syncProbability , disconnectProbability , seed ) { try { matrices = [ ] ; runtimes = [ ] ; trace = [ ] ; containerRuntimeFactory = new MockContainerRuntimeFactoryForReconnection ( ) ; for ( let i = 0 ; i < numClients ; i ++ ) { const dataStoreRuntimeN = new MockFluidDataStoreRuntime ( ) ; const containerRuntimeN = containerRuntimeFactory . createContainerRuntime ( dataStoreRuntimeN ) ; const servicesN = { deltaConnection : containerRuntimeN . createDeltaConnection ( ) , objectStorage : new MockStorage ( ) } ; const matrixN = new SharedMatrix ( dataStoreRuntimeN , `matrix-${ i }` , SharedMatrixFactory . Attributes ) ; matrixN . connect ( servicesN ) ; matrices . push ( matrixN ) ; runtimes . push ( containerRuntimeN ) ; } const matrix0 = matrices [ 0 ] ; const float64 = new Random ( seed ) . float64 ; const int32 = ( max = 0x7FFFFFFF ) => ( float64 ( ) * max ) | 0 ; const values = ( n ) => new Array ( n ) . fill ( 0 ) . map ( ( ) => int32 ( 100 ) ) ; const setCells = ( matrixIndex , row , col , colCount , values ) => { const matrix = matrices [ matrixIndex ] ; trace ?. push ( `matrix${ matrixIndex + 1 }.setCells(/* row: */ ${ row }, /* col: */ ${ col }, /* colCount: */ ${ colCount }, ${ JSON . stringify ( values ) });    // rowCount: ${ matrix . rowCount } colCount: ${ matrix . colCount } stride: ${ matrix . colCount } length: ${ values . length }` ) ; matrix . setCells ( row , col , colCount , values ) ; } ; { const rowCount = int32 ( 5 ) ; if ( rowCount > 0 ) { trace ?. push ( `matrix1.insertRows(/* rowStart: */ 0, /* rowCount: */ ${ rowCount });    // rowCount: ${ matrix0 . rowCount }, colCount: ${ matrix0 . colCount }` ) ; matrix0 . insertRows ( 0 , rowCount ) ; } const colCount = int32 ( 5 ) ; if ( colCount > 0 ) { trace ?. push ( `matrix1.insertCols(/* colStart: */ 0, /* colCount: */ ${ colCount });    // rowCount: ${ matrix0 . rowCount }, colCount: ${ matrix0 . colCount }` ) ; matrix0 . insertCols ( 0 , colCount ) ; } if ( colCount > 0 && rowCount > 0 ) { setCells ( 0 , 0 , 0 , colCount , new Array ( colCount * rowCount ) . fill ( 0 ) . map ( ( _ , index ) => index ) ) ; } } for ( let i = 0 ; i < numOps ; i ++ ) { const matrixIndex = int32 ( matrices . length ) ; const matrix = matrices [ matrixIndex ] ; const { rowCount , colCount } = matrix ; const row = int32 ( rowCount ) ; const col = int32 ( colCount ) ; switch ( int32 ( 7 ) ) { case 0 : { if ( rowCount > 0 ) { const numRemoved = float64 ( ) < 0.1 ? int32 ( rowCount - row - 1 ) + 1 : 1 ; trace ?. push ( `matrix${ matrixIndex + 1 }.removeRows(/* rowStart: */ ${ row }, /* rowCount: */ ${ numRemoved });    // rowCount: ${ matrix . rowCount - numRemoved }, colCount: ${ matrix . colCount }` ) ; matrix . removeRows ( row , numRemoved ) ; } break ; } case 1 : { if ( colCount > 0 ) { const numRemoved = float64 ( ) < 0.1 ? int32 ( colCount - col - 1 ) + 1 : 1 ; trace ?. push ( `matrix${ matrixIndex + 1 }.removeCols(/* colStart: */ ${ col }, /* colCount: */ ${ numRemoved });    // rowCount: ${ matrix . rowCount }, colCount: ${ matrix . colCount - numRemoved }` ) ; matrix . removeCols ( col , numRemoved ) ; } break ; } case 2 : { const numInserted = float64 ( ) < 0.2 ? int32 ( 3 ) + 1 : 1 ; trace ?. push ( `matrix${ matrixIndex + 1 }.insertRows(/* rowStart: */ ${ row }, /* rowCount: */ ${ numInserted });    // rowCount: ${ matrix . rowCount + numInserted }, colCount: ${ matrix . colCount }` ) ; matrix . insertRows ( row , numInserted ) ; if ( float64 ( ) < 0.9 ) { if ( colCount > 0 ) { setCells ( matrixIndex , row , 0 , matrix . colCount , values ( matrix . colCount * numInserted ) ) ; } } break ; } case 3 : { const numInserted = float64 ( ) < 0.2 ? int32 ( 3 ) + 1 : 1 ; trace ?. push ( `matrix${ matrixIndex + 1 }.insertCols(/* colStart: */ ${ col }, /* colCount: */ ${ numInserted });    // rowCount: ${ matrix . rowCount }, colCount: ${ matrix . colCount + numInserted }` ) ; matrix . insertCols ( col , numInserted ) ; if ( float64 ( ) < 0.9 ) { if ( rowCount > 0 ) { setCells ( matrixIndex , 0 , col , numInserted , values ( matrix . rowCount * numInserted ) ) ; } } break ; } default : { if ( rowCount > 0 && colCount > 0 ) { const stride = int32 ( colCount - col - 1 ) + 1 ; const length = ( int32 ( rowCount - row - 1 ) + 1 ) * stride ; setCells ( matrixIndex , row , col , stride , values ( length ) ) ; } break ; } } if ( float64 ( ) < disconnectProbability ) { if ( ! runtimes [ matrixIndex ] . connected ) { trace ?. push ( `containerRuntime${ matrixIndex + 1 }.connected = true;` ) ; runtimes [ matrixIndex ] . connected = true ; } trace ?. push ( `containerRuntime${ matrixIndex + 1 }.connected = false;` ) ; runtimes [ matrixIndex ] . connected = false ; } if ( float64 ( ) < syncProbability ) { await expect ( ) ; } } await expect ( ) ; } catch ( error ) { for ( const s of trace ) { console . log ( s ) ; } for ( const m of matrices ) { console . log ( m . toString ( ) ) ; } throw error ; } } for ( const { numClients , numOps , syncProbability , disconnectProbability , seed } of [ { numClients : 2 , numOps : 200 , syncProbability : 0.3 , disconnectProbability : 0 , seed : 0x84d43a0a } , { numClients : 3 , numOps : 200 , syncProbability : 0.1 , disconnectProbability : 0 , seed : 0x655c763b } , { numClients : 5 , numOps : 200 , syncProbability : 0.0 , disconnectProbability : 0 , seed : 0x2f98736d } , { numClients : 2 , numOps : 200 , syncProbability : 0.2 , disconnectProbability : 0.4 , seed : 0x84d43a0a } , ] ) { it ( `Stress (numClients=${ numClients } numOps=${ numOps } syncProbability=${ syncProbability } disconnectProbability=${ disconnectProbability } seed=0x${ seed . toString ( 16 ) . padStart ( 8 , "0" ) })` , async function ( ) { this . timeout ( 20000 ) ; await stress ( numClients , numOps , syncProbability , disconnectProbability , seed ) ; } ) ; } it ( "stress-loop" , async function ( ) { this . timeout ( 0 ) ; let iterations = 0 ; const start = Date . now ( ) ; while ( true ) { await stress ( 3 , 10000 , 0.1 , 0.01 , ( Math . random ( ) * 0x100000000 ) >>> 0 ) ; process . stdout . write ( matrices [ 0 ] . toString ( ) ) ; process . stdout . write ( `Stress loop: ${ ++ iterations } iterations completed - Total Elapsed: ${ ( ( Date . now ( ) - start ) / 1000 ) . toFixed ( 2 ) }s\n` ) ; } } ) ; } ) ; } ) ;