/* ======================== idLobby::ApplySnapshotDeltaInternal ======================== */ bool idLobby::ApplySnapshotDeltaInternal( int p, int snapshotNumber ) { assert( lobbyType == GetActingGameStateLobbyType() ); if( !verify( p >= 0 && p < peers.Num() ) ) { return false; } peer_t& peer = peers[p]; if( !peer.IsConnected() ) { return false; } // on the server, player = peer number + 1, this only works as long as we don't support clients joining and leaving during game // on the client, always 0 bool result = peer.snapProc->ApplySnapshotDelta( IsHost() ? p + 1 : 0, snapshotNumber ); if( result && IsHost() && peer.snapProc->HasPendingSnap() ) { // Send more of the pending snap if we have one for this peer. // The reason we can do this, is because we know more about this peers base state now. // And since we maxed out the optimal snap delta size, we'll now be able // to send more data, since we assume we'll get better and better delta compression as // our version of this peers base state approaches parity with the peers actual state. // We don't send immediately, since we have to coordinate sending snaps for all peers in same place considering jobs. peer.needToSubmitPendingSnap = true; NET_VERBOSESNAPSHOT_PRINT( "NET: Sent more unsent snapshot data to peer %d for snapshot %d\n", p, snapshotNumber ); } return result; }
/* ======================== idSnapshotProcessor::ApplySnapshotDelta Apply a snapshot delta to our current basestate, and make that the new base. We can remove all deltas that refer to the basetate we just removed. ======================== */ bool idSnapshotProcessor::ApplySnapshotDelta( int visIndex, int snapshotNumber ) { NET_VERBOSESNAPSHOT_PRINT_LEVEL( 6, va( "idSnapshotProcessor::ApplySnapshotDelta snapshotNumber: %d\n", snapshotNumber ) ); // Sanity check deltas SanityCheckDeltas(); // dump any deltas older than the acknoweledged snapshot, which should only happen if there is packet loss deltas.RemoveOlderThan( snapshotNumber ); if( deltas.Num() == 0 || deltas.ItemSequence( 0 ) != snapshotNumber ) { // this means the snapshot was either already acknowledged or came out of order // On the server, this can happen because the client is continuously/redundantly sending acks // Once the server has ack'd a certain base sequence, it will need to ignore all the redundant ones. // On the client, this will only happen due to out of order, or dropped packets. if( !common->IsServer() ) { // these should be printed every time on the clients // printing on server is not useful / results in tons of spam if( deltas.Num() == 0 ) { NET_VERBOSESNAPSHOT_PRINT( "NET: Got snapshot but ignored... deltas.Num(): %d snapshotNumber: %d \n", deltas.Num(), snapshotNumber ); } else { NET_VERBOSESNAPSHOT_PRINT( "NET: Got snapshot but ignored... deltas.ItemSequence( 0 ): %d != snapshotNumber: %d \n ", deltas.ItemSequence( 0 ), snapshotNumber ); for( int i = 0; i < deltas.Num(); i++ ) { NET_VERBOSESNAPSHOT_PRINT( "%d ", deltas.ItemSequence( i ) ); } NET_VERBOSESNAPSHOT_PRINT( "\n" ); } } return false; } int deltaSequence = 0; int deltaBaseSequence = 0; PeekDeltaSequence( ( const char* )deltas.ItemData( 0 ), deltas.ItemLength( 0 ), deltaSequence, deltaBaseSequence ); assert( deltaSequence == snapshotNumber ); // Make sure compressed sequence number matches that in data queue assert( baseSequence == deltaBaseSequence ); // If this delta isn't based off of our currently ack'd basestate, something is trashed... assert( deltaSequence > baseSequence ); if( baseSequence != deltaBaseSequence ) { // NOTE - This should no longer happen with recent fixes. // We should probably disconnect from the server if this happens. (packets are trashed most likely) NET_VERBOSESNAPSHOT_PRINT( "NET: Got snapshot %d but baseSequence does not match. baseSequence: %d deltaBaseSequence: %d. \n", snapshotNumber, baseSequence, deltaBaseSequence ); return false; } // Apply this delta to our base state if( ApplyDeltaToSnapshot( baseState, ( const char* )deltas.ItemData( 0 ), deltas.ItemLength( 0 ), visIndex ) ) { lastFullSnapBaseSequence = deltaSequence; } baseSequence = deltaSequence; // This is now our new base sequence // Remove deltas that we no longer need RemoveDeltasForOldBaseSequence(); // Sanity check deltas SanityCheckDeltas(); return true; }
/* ======================== idSnapshotProcessor::ReceiveSnapshotDelta NOTE: we use ReadDeltaForJob twice, once to build the same base as the server (based on server acks, down ApplySnapshotDelta), and another time to apply the snapshot we just received could we avoid the double apply by keeping outSnap cached in memory and avoid rebuilding it from a delta when the next one comes around? ======================== */ bool idSnapshotProcessor::ReceiveSnapshotDelta( const byte* deltaData, int deltaLength, int visIndex, int& outSeq, int& outBaseSeq, idSnapShot& outSnap, bool& fullSnap ) { fullSnap = false; int deltaSequence = 0; int deltaBaseSequence = 0; // Get the sequence of this delta, and the base sequence it is delta'd from PeekDeltaSequence( ( const char* )deltaData, deltaLength, deltaSequence, deltaBaseSequence ); //idLib::Printf("Incoming snapshot: %i, %i\n", deltaSequence, deltaBaseSequence ); if( deltaSequence <= snapSequence ) { NET_VERBOSESNAPSHOT_PRINT( "Rejecting old delta: %d (snapSequence: %d \n", deltaSequence, snapSequence ); return false; // Completely reject older out of order deltas } // Bring the base state up to date with the basestate this delta was compared to ApplySnapshotDelta( visIndex, deltaBaseSequence ); // Once we get here, our base state should be caught up to that of the server assert( baseSequence == deltaBaseSequence ); // Save the new delta if( net_skipClientDeltaAppend.GetBool() || !deltas.Append( deltaSequence, deltaData, deltaLength ) ) { // This can happen if the delta queues get desync'd between the server and client. // With recent fixes, this should be extremely rare, or impossible. // Just in case this happens, we can recover by assuming we didn't even receive this delta. idLib::Printf( "NET: ReceiveSnapshotDelta: No room to append delta %d/%d \n", deltaSequence, deltaBaseSequence ); return false; } // Update our snapshot sequence number to the newer one we just got (now that it's safe) snapSequence = deltaSequence; if( deltas.Num() > 10 ) { NET_VERBOSESNAPSHOT_PRINT( "NET: ReceiveSnapshotDelta: deltas.Num() > 10: %d\n ", deltas.Num() ); for( int i = 0; i < deltas.Num(); i++ ) { NET_VERBOSESNAPSHOT_PRINT( "%d ", deltas.ItemSequence( i ) ); } NET_VERBOSESNAPSHOT_PRINT( "\n" ); } if( baseSequence != deltaBaseSequence ) { // NOTE - With recent fixes, this should no longer be possible unless the delta is trashed // We should probably disconnect from the server when this happens now. static bool failed = false; if( !failed ) { idLib::Printf( "NET: incorrect base state? not sure how this can happen... baseSequence: %d deltaBaseSequence: %d \n", baseSequence, deltaBaseSequence ); } failed = true; return false; } // Copy out the current deltas sequence values to caller outSeq = deltaSequence; outBaseSeq = deltaBaseSequence; if( baseSequence < 50 && net_debugBaseStates.GetBool() ) { idLib::Printf( "NET: Proper basestate... baseSequence: %d deltaBaseSequence: %d \n", baseSequence, deltaBaseSequence ); } // Make a copy of the basestate the server used to create this delta, and then apply and return it outSnap = baseState; fullSnap = ApplyDeltaToSnapshot( outSnap, ( const char* )deltaData, deltaLength, visIndex ); // We received a new delta return true; }
/* ======================== idSnapShot::WriteObject ======================== */ void idSnapShot::WriteObject( idFile* file, int visIndex, objectState_t* newState, objectState_t* oldState, int& lastobjectNum ) { assert( newState != NULL || oldState != NULL ); bool visChange = false; // visibility changes will be signified with a 0xffff state size bool visSendState = false; // the state is sent when an entity is no longer stale // Compute visibility changes // (we need to do this before writing out object id, because we may not need to write out the id if we early out) // (when we don't write out the id, we assume this is an "ack" when we deserialize the objects) if( newState != NULL && oldState != NULL ) { // Check visibility assert( newState->objectNum == oldState->objectNum ); if( visIndex > 0 ) { bool oldVisible = ( oldState->visMask & ( 1 << visIndex ) ) != 0; bool newVisible = ( newState->visMask & ( 1 << visIndex ) ) != 0; // Force visible if we need to either create or destroy this object newVisible |= ( newState->buffer.Size() == 0 ) != ( oldState->buffer.Size() == 0 ); if( !oldVisible && !newVisible ) { // object is stale and ack'ed for this client, write nothing (see 'same object' below) return; } else if( oldVisible && !newVisible ) { NET_VERBOSESNAPSHOT_PRINT( "object %d to client %d goes stale\n", newState->objectNum, visIndex ); visChange = true; visSendState = false; } else if( !oldVisible && newVisible ) { NET_VERBOSESNAPSHOT_PRINT( "object %d to client %d no longer stale\n", newState->objectNum, visIndex ); visChange = true; visSendState = true; } } // Same object, write a delta (never early out during vis changes) if( !visChange && newState->buffer.Size() == oldState->buffer.Size() && ( ( newState->buffer.Ptr() == oldState->buffer.Ptr() ) || memcmp( newState->buffer.Ptr(), oldState->buffer.Ptr(), newState->buffer.Size() ) == 0 ) ) { // same state, write nothing return; } } // Get the id of the object we are writing out uint16 objectNum; if( newState != NULL ) { objectNum = newState->objectNum; } else if( oldState != NULL ) { objectNum = oldState->objectNum; } else { objectNum = 0; } assert( objectNum == 0 || objectNum > lastobjectNum ); // Write out object id (using delta) uint16 objectDelta = objectNum - lastobjectNum; file->WriteBig( objectDelta ); lastobjectNum = objectNum; if( newState == NULL ) { // Deleted, write 0 size assert( oldState != NULL ); file->WriteBig<objectSize_t>( 0 ); } else if( oldState == NULL ) { // New object, write out full state assert( newState != NULL ); // delta against an empty snap file->WriteBig( newState->buffer.Size() ); file->Write( newState->buffer.Ptr(), newState->buffer.Size() ); } else { // Compare to last object assert( newState != NULL && oldState != NULL ); assert( newState->objectNum == oldState->objectNum ); if( visChange ) { // fake size indicates vis state change // NOTE: we may still send a real size and a state below, for 'no longer stale' transitions // TMP: send 0xFFFF for going stale and 0xFFFF - 1 for no longer stale file->WriteBig<objectSize_t>( visSendState ? SIZE_NOT_STALE : SIZE_STALE ); } if( !visChange || visSendState ) { objectSize_t compareSize = Min( newState->buffer.Size(), oldState->buffer.Size() ); // Get the number of bytes that overlap file->WriteBig( newState->buffer.Size() ); // Write new size // Compare bytes that overlap for( objectSize_t b = 0; b < compareSize; b++ ) { file->WriteBig<byte>( ( 0xFF + 1 + newState->buffer[b] - oldState->buffer[b] ) & 0xFF ); } // Write leftover if( newState->buffer.Size() > compareSize ) { file->Write( newState->buffer.Ptr() + oldState->buffer.Size(), newState->buffer.Size() - compareSize ); } } } #ifdef SNAPSHOT_CHECKSUMS if( ( !visChange || visSendState ) && newState != NULL ) { assert( newState->buffer.Size() > 0 ); unsigned int checksum = MD5_BlockChecksum( newState->buffer.Ptr(), newState->buffer.Size() ); file->WriteBig( checksum ); } #endif }
/* ======================== idSnapShot::ReadDelta ======================== */ bool idSnapShot::ReadDelta( idFile* file, int visIndex ) { file->ReadBig( time ); int objectNum = 0; uint16 delta = 0; while( file->ReadBig( delta ) == sizeof( delta ) ) { objectNum += delta; if( objectNum >= 0xFFFF ) { // full delta return true; } objectState_t& state = FindOrCreateObjectByID( objectNum ); objectSize_t newsize = 0; file->ReadBig( newsize ); if( newsize == SIZE_STALE ) { NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d goes stale\n", objectNum ); // sanity bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0; if( !oldVisible ) { NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected already stale\n" ); } state.visMask &= ~( 1 << visIndex ); state.stale = true; // We need to make sure we haven't freed stale objects. assert( state.buffer.Size() > 0 ); // no more data continue; } else if( newsize == SIZE_NOT_STALE ) { NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d no longer stale\n", objectNum ); // sanity bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0; if( oldVisible ) { NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected not stale\n" ); } state.visMask |= ( 1 << visIndex ); state.stale = false; // the latest state is packed in, get the new size and continue reading the new state file->ReadBig( newsize ); } if( newsize == 0 ) { // object deleted state.buffer._Release(); } else { objectBuffer_t newbuffer( newsize ); objectSize_t compareSize = Min( newsize, state.buffer.Size() ); for( objectSize_t i = 0; i < compareSize; i++ ) { uint8 delta = 0; file->ReadBig<byte>( delta ); newbuffer[i] = state.buffer[i] + delta; } if( newsize > compareSize ) { file->Read( newbuffer.Ptr() + compareSize, newsize - compareSize ); } state.buffer = newbuffer; state.changedCount++; } #ifdef SNAPSHOT_CHECKSUMS if( state.buffer.Size() > 0 ) { unsigned int checksum = 0; file->ReadBig( checksum ); assert( checksum == MD5_BlockChecksum( state.buffer.Ptr(), state.buffer.Size() ) ); } #endif } // partial delta return false; }
/* ======================== idSnapShot::ReadDeltaForJob ======================== */ bool idSnapShot::ReadDeltaForJob( const char* deltaMem, int deltaSize, int visIndex, idSnapShot* templateStates ) { bool report = net_verboseSnapshotReport.GetBool(); net_verboseSnapshotReport.SetBool( false ); lzwCompressionData_t lzwData; idZeroRunLengthCompressor rleCompressor; idLZWCompressor lzwCompressor( &lzwData ); int bytesRead = 0; // how many uncompressed bytes we read in. Used to figure out compression ratio lzwCompressor.Start( ( uint8* )deltaMem, deltaSize ); // Skip past sequence and baseSequence int sequence = 0; int baseSequence = 0; lzwCompressor.ReadAgnostic( sequence ); lzwCompressor.ReadAgnostic( baseSequence ); lzwCompressor.ReadAgnostic( time ); bytesRead += sizeof( int ) * 3; int objectNum = 0; uint16 delta = 0; while( lzwCompressor.ReadAgnostic( delta, true ) == sizeof( delta ) ) { bytesRead += sizeof( delta ); objectNum += delta; if( objectNum >= 0xFFFF ) { // full delta if( net_verboseSnapshotCompression.GetBool() ) { float compRatio = static_cast<float>( deltaSize ) / static_cast<float>( bytesRead ); idLib::Printf( "Snapshot (%d/%d). ReadSize: %d DeltaSize: %d Ratio: %.3f\n", sequence, baseSequence, bytesRead, deltaSize, compRatio ); } return true; } objectState_t& state = FindOrCreateObjectByID( objectNum ); objectSize_t newsize = 0; lzwCompressor.ReadAgnostic( newsize ); bytesRead += sizeof( newsize ); if( newsize == SIZE_STALE ) { NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d goes stale\n", objectNum ); // sanity bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0; if( !oldVisible ) { NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected already stale\n" ); } state.visMask &= ~( 1 << visIndex ); state.stale = true; // We need to make sure we haven't freed stale objects. assert( state.buffer.Size() > 0 ); // no more data continue; } else if( newsize == SIZE_NOT_STALE ) { NET_VERBOSESNAPSHOT_PRINT( "read delta: object %d no longer stale\n", objectNum ); // sanity bool oldVisible = ( state.visMask & ( 1 << visIndex ) ) != 0; if( oldVisible ) { NET_VERBOSESNAPSHOT_PRINT( "ERROR: unexpected not stale\n" ); } state.visMask |= ( 1 << visIndex ); state.stale = false; // the latest state is packed in, get the new size and continue reading the new state lzwCompressor.ReadAgnostic( newsize ); bytesRead += sizeof( newsize ); } objectState_t* objTemplateState = templateStates->FindObjectByID( objectNum ); if( newsize == 0 ) { // object deleted: reset state now so next one to use it doesn't have old data state.deleted = false; state.stale = false; state.changedCount = 0; state.expectedSequence = 0; state.visMask = 0; state.buffer._Release(); state.createdFromTemplate = false; if( objTemplateState != NULL && objTemplateState->buffer.Size() && objTemplateState->expectedSequence < baseSequence ) { idLib::PrintfIf( net_ssTemplateDebug.GetBool(), "Clearing old template state[%d] [%d<%d]\n", objectNum, objTemplateState->expectedSequence, baseSequence ); objTemplateState->deleted = false; objTemplateState->stale = false; objTemplateState->changedCount = 0; objTemplateState->expectedSequence = 0; objTemplateState->visMask = 0; objTemplateState->buffer._Release(); } } else { // new state? bool debug = false; if( state.buffer.Size() == 0 ) { state.createdFromTemplate = true; // Brand new state if( objTemplateState != NULL && objTemplateState->buffer.Size() > 0 && sequence >= objTemplateState->expectedSequence ) { idLib::PrintfIf( net_ssTemplateDebug.GetBool(), "\nAdding basestate for new object %d (for SS %d/%d. obj base created in ss %d) deltaSize: %d\n", objectNum, sequence, baseSequence, objTemplateState->expectedSequence, deltaSize ); state.buffer = objTemplateState->buffer; if( net_ssTemplateDebug.GetBool() ) { state.Print( "SPAWN STATE" ); debug = true; PrintAlign( "DELTA STATE" ); } } else if( net_ssTemplateDebug.GetBool() ) { idLib::Printf( "\nNew snapobject[%d] in snapshot %d/%d but no basestate found locally so creating new\n", objectNum, sequence, baseSequence ); } } else { state.createdFromTemplate = false; } // the buffer shrank or stayed the same objectBuffer_t newbuffer( newsize ); rleCompressor.Start( NULL, &lzwCompressor, newsize ); objectSize_t compareSize = Min( state.buffer.Size(), newsize ); for( objectSize_t i = 0; i < compareSize; i++ ) { byte b = rleCompressor.ReadByte(); newbuffer[i] = state.buffer[i] + b; if( debug && InDebugRange( i ) ) { idLib::Printf( "%02X", b ); } } // Catch leftover if( newsize > compareSize ) { rleCompressor.ReadBytes( newbuffer.Ptr() + compareSize, newsize - compareSize ); if( debug ) { for( objectSize_t i = compareSize; i < newsize; i++ ) { if( InDebugRange( i ) ) { idLib::Printf( "%02X", newbuffer[i] ); } } } } state.buffer = newbuffer; state.changedCount = sequence; bytesRead += sizeof( byte ) * newsize; if( debug ) { idLib::Printf( "\n" ); state.Print( "NEW STATE" ); } if( report ) { idLib::Printf( " Obj %d Compressed: Size %d \n", objectNum, rleCompressor.CompressedSize() ); } } #ifdef SNAPSHOT_CHECKSUMS extern uint32 SnapObjChecksum( const uint8 * data, int length ); if( state.buffer.Size() > 0 ) { uint32 checksum = 0; lzwCompressor.ReadAgnostic( checksum ); bytesRead += sizeof( checksum ); if( !verify( checksum == SnapObjChecksum( state.buffer.Ptr(), state.buffer.Size() ) ) ) { idLib::Error( " Invalid snapshot checksum" ); } } #endif } // partial delta return false; }
/* ======================== idLobby::SendCompletedPendingSnap ======================== */ void idLobby::SendCompletedPendingSnap( int p ) { assert( lobbyType == GetActingGameStateLobbyType() ); int time = Sys_Milliseconds(); peer_t& peer = peers[p]; if( !peer.IsConnected() ) { return; } if( peer.snapProc == NULL || !peer.snapProc->PendingSnapReadyToSend() ) { return; } // If we have a pending snap ready to send, we better have a pending snap assert( peer.snapProc->HasPendingSnap() ); // Get the snap data blob now, even if we don't send it. // This is somewhat wasteful, but we have to do this to keep the snap job pipe ready to keep doing work // If we don't do this, this peer will cause other peers to be starved of snapshots, when they may very well be ready to send a snap byte buffer[ MAX_SNAP_SIZE ]; int maxLength = sizeof( buffer ) - peer.packetProc->GetReliableDataSize() - 128; int size = peer.snapProc->GetPendingSnapDelta( buffer, maxLength ); if( !CanSendMoreData( p ) ) { return; } // Can't send anymore snapshots until all fragments are sent if( peer.packetProc->HasMoreFragments() ) { return; } // If the peer doesn't have the latest resource list, send it to him before sending any new snapshots if( SendResources( p ) ) { return; } int timeFromJobSub = time - peer.lastSnapJobTime; int timeFromLastSend = time - peer.lastSnapTime; if( timeFromLastSend > 0 ) { peer.snapHz = 1000.0f / ( float )timeFromLastSend; } else { peer.snapHz = 0.0f; } if( net_snapshot_send_warntime.GetInteger() > 0 && peer.lastSnapTime != 0 && net_snapshot_send_warntime.GetInteger() < timeFromLastSend ) { idLib::Printf( "NET: Took %d ms to send peer %d snapshot\n", timeFromLastSend, p ); } if( peer.throttleSnapsForXSeconds != 0 ) { if( time < peer.throttleSnapsForXSeconds ) { return; } // If we were trying to recover ping, see if we succeeded if( peer.recoverPing != 0 ) { if( peer.lastPingRtt >= peer.recoverPing ) { peer.failedPingRecoveries++; } else { const int peer_throttle_minSnapSeq = session->GetTitleStorageInt( "net_peer_throttle_minSnapSeq", net_peer_throttle_minSnapSeq.GetInteger() ); if( peer.snapProc->GetFullSnapBaseSequence() > idSnapshotProcessor::INITIAL_SNAP_SEQUENCE + peer_throttle_minSnapSeq ) { // If throttling recovered the ping int maxRate = common->GetSnapRate() * session->GetTitleStorageInt( "net_peer_throttle_maxSnapRate", net_peer_throttle_maxSnapRate.GetInteger() ); peer.throttledSnapRate = idMath::ClampInt( common->GetSnapRate(), maxRate, peer.throttledSnapRate + common->GetSnapRate() ); } } } peer.throttleSnapsForXSeconds = 0; } peer.lastSnapTime = time; if( size != 0 ) { if( size > 0 ) { NET_VERBOSESNAPSHOT_PRINT_LEVEL( 3, va( "NET: (peer %d) Sending snapshot %d delta'd against %d. Since JobSub: %d Since LastSend: %d. Size: %d\n", p, peer.snapProc->GetSnapSequence(), peer.snapProc->GetBaseSequence(), timeFromJobSub, timeFromLastSend, size ) ); ProcessOutgoingMsg( p, buffer, size, false, 0 ); } else if( size < 0 ) // Size < 0 indicates the delta buffer filled up { // There used to be code here that would disconnect peers if they were in game and filled up the buffer // This was causing issues in the playtests we were running (Doom 4 MP) and after some conversation // determined that it was not needed since a timeout mechanism has been added since ProcessOutgoingMsg( p, buffer, -size, false, 0 ); if( peer.snapProc != NULL ) { NET_VERBOSESNAPSHOT_PRINT( "NET: (peerNum: %d - name: %s) Resending last snapshot delta %d because his delta list filled up. Since JobSub: %d Since LastSend: %d Delta Size: %d\n", p, GetPeerName( p ), peer.snapProc->GetSnapSequence(), timeFromJobSub, timeFromLastSend, size ); } } } // We calculate what our outgoing rate was for each sequence, so we can have a relative comparison // for when the client reports what his downstream was in the same timeframe if( IsHost() && peer.snapProc != NULL && peer.snapProc->GetSnapSequence() > 0 ) { //NET_VERBOSE_PRINT("^8 %i Rate: %.2f SnapSeq: %d GetBaseSequence: %d\n", lastAppendedSequence, peer.packetProc->GetOutgoingRateBytes(), peer.snapProc->GetSnapSequence(), peer.snapProc->GetBaseSequence() ); peer.sentBpsHistory[ peer.snapProc->GetSnapSequence() % MAX_BPS_HISTORY ] = peer.packetProc->GetOutgoingRateBytes(); } }