├── LICENSE ├── README.md ├── build.bat ├── build.sh ├── fast_pipe.h └── termbench.cpp /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Casey Muratori 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TermBench V2 2 | 3 | This is a simple benchmark you can use to see how your terminal sinks large outputs. While it cannot time how long it takes your terminal to render (since it has no idea), it _can_ time how long it takes your terminal to accept the data, which is what termbench measures. For a full benchmark, you would need to also time how long your renderer takes to complete rendering after the sink finishes. 4 | 5 | # Usage 6 | 7 | On slow terminals, you will want to run termbench like this: 8 | 9 | ``` 10 | termbench_release_clang small 11 | ``` 12 | 13 | This will run very small data sizes (~1 megabyte) so that the terminal has a prayer of completing the benchmark in a reasonable amount of time. This is the recommended setting for things like cmd.exe or Windows Terminal. 14 | 15 | For terminals that have reasonable performance, you run it like this: 16 | 17 | ``` 18 | termbench_release_clang 19 | ``` 20 | 21 | for the regular benchmark sizes, or like this 22 | 23 | ``` 24 | termbench_release_clang large 25 | ``` 26 | 27 | for larger benchmark sizes (if you want more of a stress test than normal). 28 | 29 | # Expected Results 30 | 31 | On modern Windows machines with memory bandwidth in the 10-20gb/s range, the expected throughput for these tests would be in the 0.5-2.0gb/s range for a reasonable terminal. Numbers significantly higher than that might indicate a well-optimized terminal, and numbers significantly lower than that might indicate a poorly written terminal. 32 | 33 | Termbench has not yet been tested on Linux, so we do not have expected bandwidth numbers at this time. 34 | 35 | Obviously, throughput numbers depend greatly on the underlying hardware, and the operating system pipe behavior, so take these expected values with a grain of salt. 36 | -------------------------------------------------------------------------------- /build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set BaseFile=termbench.cpp 4 | set CLLinkFlags=-incremental:no -opt:ref -machine:x64 -manifest:no -subsystem:console kernel32.lib user32.lib 5 | set CLCompileFlags=-Zi -d2Zi+ -Gy -GF -GR- -EHs- -EHc- -EHa- -WX -W4 -nologo -FC -Gm- -diagnostics:column -fp:except- -fp:fast 6 | set CLANGCompileFlags=-g 7 | set CLANGLinkFlags=-fuse-ld=lld -Wl,-subsystem:console,kernel32.lib,user32.lib 8 | 9 | echo ----------------- 10 | echo Building debug: 11 | call cl -Fetermbench_debug_msvc.exe -Od %CLCompileFlags% %BaseFile% /link %CLLinkFlags% -RELEASE 12 | call clang++ %CLANGCompileFlags% %CLANGLinkFlags% %BaseFile% -o termbench_debug_clang.exe 13 | 14 | echo ----------------- 15 | echo Building release: 16 | call cl -Fetermbench_release_msvc.exe -Oi -Oxb2 -O2 %CLCompileFlags% %BaseFile% /link %CLLinkFlags% -RELEASE 17 | call clang++ -O3 %CLANGCompileFlags% %CLANGLinkFlags% %BaseFile% -o termbench_release_clang.exe 18 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BaseFile=termbench.cpp 3 | 4 | CLANG=$(which clang++) 5 | if [[ -x "${CLANG}" ]] 6 | then 7 | CLLinkFlags="-Wformat" 8 | CLCompileFlags="-O3 -Ofast" 9 | CC="${CLANG}" 10 | output=termbench_release_clang 11 | else 12 | echo "ABORTING: no compiler detected" 13 | fi 14 | 15 | if [[ -x "${CC}" ]] 16 | then 17 | echo Building release: ./${output} 18 | "${CC}" -O3 ${CLCompileFlags} ${CLLinkFlags} ${BaseFile} -o "${output}" 19 | strip "${output}" 20 | fi 21 | -------------------------------------------------------------------------------- /fast_pipe.h: -------------------------------------------------------------------------------- 1 | /* NOTE(casey): 2 | 3 | "Fast pipe" is a technique to bypass the (very slow) Windows conio subsystem. 4 | Since the Windows kernel has reasonably fast pipes, if the calling process 5 | opens a named pipe under the "fastpipe########" label, where the #'s are 6 | replaced with our process ID, then we know that the shell on the other 7 | side can accept direct output. stdin and stdout are then remapped to 8 | that pipe. 9 | 10 | To use, #include this file in your program, and insert 11 | 12 | USE_FAST_PIPE_IF_AVAILABLE(); 13 | 14 | at the very start of your program (ideally the first line in main). 15 | It will also optionally return non-zero when the fast pipe is available, 16 | in case your program wishes to take special action when a fast pipe 17 | exists: 18 | 19 | int FastPipeIsAvailable = USE_FAST_PIPE_IF_AVAILABLE(); 20 | 21 | No further changes to the program should be necessary. Note that 22 | stderr is, by default, still mapped to the slow conio pipe. 23 | */ 24 | 25 | #if _WIN32 26 | #include 27 | #ifndef _VC_NODEFAULTLIB 28 | #include 29 | #include 30 | #include 31 | #endif 32 | #pragma comment (lib, "kernel32.lib") 33 | #pragma comment (lib, "user32.lib") 34 | static int USE_FAST_PIPE_IF_AVAILABLE() 35 | { 36 | int Result = 0; 37 | 38 | wchar_t PipeName[32]; 39 | wsprintfW(PipeName, L"\\\\.\\pipe\\fastpipe%x", GetCurrentProcessId()); 40 | HANDLE FastPipe = CreateFileW(PipeName, GENERIC_READ|GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0); 41 | if(FastPipe != INVALID_HANDLE_VALUE) 42 | { 43 | SetStdHandle(STD_OUTPUT_HANDLE, FastPipe); 44 | SetStdHandle(STD_INPUT_HANDLE, FastPipe); 45 | 46 | #ifndef _VC_NODEFAULTLIB 47 | int StdOut = _open_osfhandle((intptr_t)FastPipe, O_WRONLY|O_TEXT); 48 | int StdIn = _open_osfhandle((intptr_t)FastPipe, O_RDONLY|O_TEXT); 49 | 50 | _dup2(StdOut, _fileno(stdout)); 51 | _dup2(StdIn, _fileno(stdin)); 52 | 53 | _close(StdOut); 54 | _close(StdIn); 55 | #endif 56 | 57 | Result = 1; 58 | } 59 | 60 | return Result; 61 | } 62 | #else 63 | #define USE_FAST_PIPE_IF_AVAILABLE(...) 0 64 | #endif 65 | -------------------------------------------------------------------------------- /termbench.cpp: -------------------------------------------------------------------------------- 1 | #define VERSION_NAME "TermMarkV2" 2 | 3 | #define ArrayCount(Array) (sizeof(Array) / sizeof((Array)[0])) 4 | typedef unsigned long long u64; 5 | 6 | #if _WIN32 7 | #include 8 | #include 9 | static u64 GetTimerFrequency(void) 10 | { 11 | LARGE_INTEGER Result; 12 | QueryPerformanceFrequency(&Result); 13 | return Result.QuadPart; 14 | } 15 | static u64 GetTimer(void) 16 | { 17 | LARGE_INTEGER Result; 18 | QueryPerformanceCounter(&Result); 19 | return Result.QuadPart; 20 | } 21 | #define WRITE_FUNCTION _write 22 | #else 23 | #include 24 | #include 25 | 26 | 27 | #if (defined(__APPLE__)) && (defined(__arm64__)) 28 | #include 29 | #else 30 | #include // For __cpuid_count() 31 | #endif 32 | 33 | 34 | 35 | static u64 GetTimerFrequency(void) 36 | { 37 | u64 Result = 1000000000ull; 38 | return Result; 39 | } 40 | static u64 GetTimer(void) 41 | { 42 | struct timespec Spec; 43 | clock_gettime(CLOCK_MONOTONIC, &Spec); 44 | u64 Result = ((u64)Spec.tv_sec * 1000000000ull) + (u64)Spec.tv_nsec; 45 | return Result; 46 | } 47 | #define WRITE_FUNCTION write 48 | #endif 49 | 50 | #include 51 | #include 52 | #include 53 | 54 | struct buffer 55 | { 56 | int MaxCount; 57 | int Count; 58 | char *Data; 59 | }; 60 | 61 | static char NumberTable[256][4]; 62 | 63 | static void AppendChar(buffer *Buffer, char Char) 64 | { 65 | if(Buffer->Count < Buffer->MaxCount) Buffer->Data[Buffer->Count++] = Char; 66 | } 67 | 68 | static void AppendString(buffer *Buffer, char const *String) 69 | { 70 | while(*String) AppendChar(Buffer, *String++); 71 | } 72 | 73 | static void AppendDecimal(buffer *Buffer, int unsigned Value) 74 | { 75 | int unsigned Remains = Value; 76 | for(int Divisor = 1000000000; 77 | Divisor > 0; 78 | Divisor /= 10) 79 | { 80 | int Digit = Remains / Divisor; 81 | Remains -= Digit*Divisor; 82 | 83 | if(Digit || (Value != Remains) || (Divisor == 1)) 84 | { 85 | AppendChar(Buffer, (char)('0' + Digit)); 86 | } 87 | } 88 | } 89 | 90 | static void AppendDouble(buffer *Buffer, double Value) 91 | { 92 | int Result = snprintf(Buffer->Data + Buffer->Count, 93 | Buffer->MaxCount - Buffer->Count, 94 | "%.04f", Value); 95 | if(Result > 0) 96 | { 97 | Buffer->Count += Result; 98 | } 99 | } 100 | 101 | static void AppendGoto(buffer *Buffer, int X, int Y) 102 | { 103 | AppendString(Buffer, "\x1b["); 104 | AppendDecimal(Buffer, Y); 105 | AppendString(Buffer, ";"); 106 | AppendDecimal(Buffer, X); 107 | AppendString(Buffer, "H"); 108 | } 109 | 110 | static void AppendColor(buffer *Buffer, int IsForeground, int unsigned Red, int unsigned Green, int unsigned Blue) 111 | { 112 | AppendString(Buffer, IsForeground ? "\x1b[38;2;" : "\x1b[48;2;"); 113 | AppendString(Buffer, NumberTable[Red & 0xff]); 114 | AppendChar(Buffer, ';'); 115 | AppendString(Buffer, NumberTable[Green & 0xff]); 116 | AppendChar(Buffer, ';'); 117 | AppendString(Buffer, NumberTable[Blue & 0xff]); 118 | AppendChar(Buffer, 'm'); 119 | } 120 | 121 | static double GetGBS(double Bytes, double Seconds) 122 | { 123 | double BytesPerGigabyte = 1024.0*1024.0*1024.0; 124 | double Result = Bytes / (BytesPerGigabyte*Seconds); 125 | return Result; 126 | } 127 | 128 | static char TerminalBuffer[64*1024*1024]; 129 | 130 | #include "fast_pipe.h" 131 | 132 | struct test_context 133 | { 134 | int OutputHandle; 135 | 136 | buffer Frame; 137 | int Width; 138 | int Height; 139 | size_t TestCount; 140 | 141 | size_t TotalWriteCount; 142 | double SecondsElapsed; 143 | 144 | u64 StartTime; 145 | u64 EndTime; 146 | }; 147 | 148 | static void RawFlushBuffer(int OutputHandle, buffer *Frame) 149 | { 150 | WRITE_FUNCTION(OutputHandle, Frame->Data, Frame->Count); 151 | Frame->Count = 0; 152 | } 153 | 154 | static void FlushBuffer(test_context *Context, buffer *Frame) 155 | { 156 | Context->TotalWriteCount += Frame->Count; 157 | RawFlushBuffer(Context->OutputHandle, Frame); 158 | } 159 | 160 | static void BeginTestTimer(test_context *Context) 161 | { 162 | Context->StartTime = GetTimer(); 163 | } 164 | 165 | static void EndTestTimer(test_context *Context) 166 | { 167 | Context->EndTime = GetTimer(); 168 | } 169 | 170 | static void FGPerChar(test_context *Context) 171 | { 172 | buffer Frame = Context->Frame; 173 | 174 | BeginTestTimer(Context); 175 | for(int FrameIndex = 0; FrameIndex < Context->TestCount; ++FrameIndex) 176 | { 177 | for(int Y = 0; Y <= Context->Height; ++Y) 178 | { 179 | AppendGoto(&Frame, 1, 1 + Y); 180 | for(int X = 0; X <= Context->Width; ++X) 181 | { 182 | int ForeRed = FrameIndex; 183 | int ForeGreen = FrameIndex + Y; 184 | int ForeBlue = FrameIndex + Y + X; 185 | 186 | AppendColor(&Frame, true, ForeRed, ForeGreen, ForeBlue); 187 | 188 | char Char = 'a' + (char)((FrameIndex + X + Y) % ('z' - 'a')); 189 | AppendChar(&Frame, Char); 190 | } 191 | } 192 | 193 | FlushBuffer(Context, &Frame); 194 | } 195 | EndTestTimer(Context); 196 | } 197 | 198 | static void FGBGPerChar(test_context *Context) 199 | { 200 | buffer Frame = Context->Frame; 201 | 202 | BeginTestTimer(Context); 203 | for(int FrameIndex = 0; FrameIndex < Context->TestCount; ++FrameIndex) 204 | { 205 | for(int Y = 0; Y <= Context->Height; ++Y) 206 | { 207 | AppendGoto(&Frame, 1, 1 + Y); 208 | for(int X = 0; X <= Context->Width; ++X) 209 | { 210 | int BackRed = FrameIndex + Y + X; 211 | int BackGreen = FrameIndex + Y; 212 | int BackBlue = FrameIndex; 213 | 214 | int ForeRed = FrameIndex; 215 | int ForeGreen = FrameIndex + Y; 216 | int ForeBlue = FrameIndex + Y + X; 217 | 218 | AppendColor(&Frame, false, BackRed, BackGreen, BackBlue); 219 | AppendColor(&Frame, true, ForeRed, ForeGreen, ForeBlue); 220 | 221 | char Char = 'a' + (char)((FrameIndex + X + Y) % ('z' - 'a')); 222 | AppendChar(&Frame, Char); 223 | } 224 | } 225 | 226 | FlushBuffer(Context, &Frame); 227 | } 228 | EndTestTimer(Context); 229 | } 230 | 231 | static void ManyLine(test_context *Context) 232 | { 233 | buffer Frame = Context->Frame; 234 | 235 | int TotalCharCount = 27; 236 | for(size_t At = 0; At < Frame.MaxCount; ++At) 237 | { 238 | char Pick = (char)(rand()%TotalCharCount); 239 | Frame.Data[At] = 'a' + Pick; 240 | if(Pick == 26) Frame.Data[At] = '\n'; 241 | } 242 | 243 | BeginTestTimer(Context); 244 | while(Context->TotalWriteCount < Context->TestCount) 245 | { 246 | Frame.Count = Frame.MaxCount; 247 | FlushBuffer(Context, &Frame); 248 | } 249 | EndTestTimer(Context); 250 | } 251 | 252 | static void LongLine(test_context *Context) 253 | { 254 | buffer Frame = Context->Frame; 255 | 256 | int TotalCharCount = 26; 257 | for(size_t At = 0; At < Frame.MaxCount; ++At) 258 | { 259 | char Pick = (char)(rand()%TotalCharCount); 260 | Frame.Data[At] = 'a' + Pick; 261 | } 262 | 263 | BeginTestTimer(Context); 264 | while(Context->TotalWriteCount < Context->TestCount) 265 | { 266 | Frame.Count = Frame.MaxCount; 267 | FlushBuffer(Context, &Frame); 268 | } 269 | EndTestTimer(Context); 270 | } 271 | 272 | typedef void test_function(test_context *Test); 273 | enum test_size 274 | { 275 | TestSize_Small, 276 | TestSize_Normal, 277 | TestSize_Large, 278 | 279 | TestSize_Count, 280 | }; 281 | static const char *SizeName[] = {"Small", "Normal", "Large"}; 282 | 283 | struct test 284 | { 285 | char const *Name; 286 | test_function *Function; 287 | size_t TestCount[TestSize_Count]; 288 | }; 289 | 290 | #define Meg (1024ull*1024ull) 291 | #define Gig (1024ull*Meg) 292 | 293 | static test Tests[] = 294 | { 295 | {"ManyLine", ManyLine, {1*Meg, 1*Gig, 16*Gig}}, 296 | {"LongLine", LongLine, {1*Meg, 1*Gig, 16*Gig}}, 297 | {"FGPerChar", FGPerChar, {512, 8192, 65536}}, 298 | {"FGBGPerChar", FGBGPerChar, {512, 8192, 65536}}, 299 | }; 300 | 301 | int main(int ArgCount, char **Args) 302 | { 303 | int BypassConhost = USE_FAST_PIPE_IF_AVAILABLE(); 304 | int VirtualTerminalSupport = 0; 305 | int TestSize = TestSize_Normal; 306 | 307 | for(int ArgIndex = 1; ArgIndex < ArgCount; ++ArgIndex) 308 | { 309 | char *Arg = Args[ArgIndex]; 310 | if(strcmp(Arg, "normal") == 0) 311 | { 312 | TestSize = TestSize_Normal; 313 | } 314 | else if(strcmp(Arg, "small") == 0) 315 | { 316 | TestSize = TestSize_Small; 317 | } 318 | else if(strcmp(Arg, "large") == 0) 319 | { 320 | TestSize = TestSize_Large; 321 | } 322 | else 323 | { 324 | fprintf(stderr, "Unrecognized argument \"%s\".\n", Arg); 325 | } 326 | } 327 | 328 | char CPU[65] = {}; 329 | 330 | #if (defined(__APPLE__)) && (defined(__arm64__)) 331 | size_t size=64; 332 | char ARMCPU[65] = {}; 333 | int64_t totalcpu = 0; 334 | int64_t cpuhigh = 0; 335 | int64_t cpulow = 0; 336 | 337 | memset(ARMCPU,0,size); 338 | size=sizeof(totalcpu); 339 | sysctlbyname("hw.ncpu", &totalcpu, &size, NULL, 0); 340 | size=sizeof(cpuhigh); 341 | sysctlbyname("hw.perflevel0.physicalcpu", &cpuhigh, &size, NULL, 0); 342 | size=sizeof(cpulow); 343 | sysctlbyname("hw.perflevel1.physicalcpu", &cpulow, &size, NULL, 0); 344 | size=64; 345 | sysctlbyname("machdep.cpu.brand_string", &ARMCPU, &size, NULL, 0); 346 | snprintf(CPU,64, "%s %lld Core Processor (HP:%lld LP:%lld)",ARMCPU,totalcpu,cpuhigh,cpulow); 347 | #else 348 | for(int SegmentIndex = 0; SegmentIndex < 3; ++SegmentIndex) 349 | { 350 | #if _WIN32 351 | __cpuid((int *)(CPU + 16*SegmentIndex), 0x80000002 + SegmentIndex); 352 | #else 353 | __get_cpuid(0x80000002 + SegmentIndex, 354 | (int unsigned *)(CPU + 16*SegmentIndex), 355 | (int unsigned *)(CPU + 16*SegmentIndex + 4), 356 | (int unsigned *)(CPU + 16*SegmentIndex + 8), 357 | (int unsigned *)(CPU + 16*SegmentIndex + 12)); 358 | #endif 359 | } 360 | 361 | #endif 362 | 363 | for(int Num = 0; Num < 256; ++Num) 364 | { 365 | buffer NumBuf = {sizeof(NumberTable[Num]), 0, NumberTable[Num]}; 366 | AppendDecimal(&NumBuf, Num); 367 | AppendChar(&NumBuf, 0); 368 | } 369 | 370 | #if _WIN32 371 | int OutputHandle = _fileno(stdout); 372 | _setmode(1, _O_BINARY); 373 | 374 | if(!BypassConhost) 375 | { 376 | HANDLE TerminalOut = GetStdHandle(STD_OUTPUT_HANDLE); 377 | 378 | DWORD WinConMode = 0; 379 | DWORD EnableVirtualTerminalProcessing = 0x0004; 380 | VirtualTerminalSupport = (GetConsoleMode(TerminalOut, &WinConMode) && 381 | SetConsoleMode(TerminalOut, (WinConMode & ~(ENABLE_ECHO_INPUT|ENABLE_LINE_INPUT)) | 382 | EnableVirtualTerminalProcessing)); 383 | } 384 | #else 385 | int OutputHandle = STDOUT_FILENO; 386 | #endif 387 | 388 | 389 | u64 Freq = GetTimerFrequency(); 390 | 391 | int Width = 80; 392 | int Height = 24; 393 | 394 | buffer Frame = {sizeof(TerminalBuffer), 0, TerminalBuffer}; 395 | 396 | test_context Contexts[ArrayCount(Tests)] = {}; 397 | for(int TestIndex = 0; TestIndex < ArrayCount(Tests); ++TestIndex) 398 | { 399 | test Test = Tests[TestIndex]; 400 | 401 | test_context *Context = Contexts + TestIndex; 402 | Context->OutputHandle = OutputHandle; 403 | Context->Frame = Frame; 404 | Context->Width = Width; 405 | Context->Height = Height; 406 | Context->TestCount = Test.TestCount[TestSize]; 407 | 408 | Test.Function(Context); 409 | Context->SecondsElapsed = (double)(Context->EndTime - Context->StartTime) / (double)Freq; 410 | } 411 | 412 | AppendColor(&Frame, 0, 0, 0, 0); 413 | AppendColor(&Frame, 1, 255, 255, 255); 414 | AppendString(&Frame, "\x1b[0m"); 415 | for(int ReturnIndex = 0; ReturnIndex < 1024; ++ReturnIndex) 416 | { 417 | AppendString(&Frame, "\n"); 418 | } 419 | 420 | AppendString(&Frame, "CPU: "); 421 | AppendString(&Frame, CPU); 422 | AppendString(&Frame, "\n"); 423 | AppendString(&Frame, "VT support: "); 424 | AppendString(&Frame, VirtualTerminalSupport ? "yes" : "no"); 425 | AppendString(&Frame, "\n"); 426 | 427 | double TotalSeconds = 0.0; 428 | size_t TotalBytes = 0; 429 | for(int TestIndex = 0; TestIndex < ArrayCount(Tests); ++TestIndex) 430 | { 431 | test Test = Tests[TestIndex]; 432 | test_context Context = Contexts[TestIndex]; 433 | 434 | AppendString(&Frame, Test.Name); 435 | AppendString(&Frame, ": "); 436 | AppendDouble(&Frame, Context.SecondsElapsed); 437 | AppendString(&Frame, "s ("); 438 | AppendDouble(&Frame, GetGBS((double)Context.TotalWriteCount, Context.SecondsElapsed)); 439 | AppendString(&Frame, "gb/s)\n"); 440 | 441 | TotalSeconds += Context.SecondsElapsed; 442 | TotalBytes += Context.TotalWriteCount; 443 | } 444 | 445 | AppendString(&Frame, VERSION_NAME); 446 | AppendString(&Frame, " "); 447 | AppendString(&Frame, SizeName[TestSize]); 448 | AppendString(&Frame, ": "); 449 | AppendDouble(&Frame, TotalSeconds); 450 | AppendString(&Frame, "s ("); 451 | AppendDouble(&Frame, GetGBS((double)TotalBytes, TotalSeconds)); 452 | AppendString(&Frame, "gb/s)\n"); 453 | 454 | RawFlushBuffer(OutputHandle, &Frame); 455 | } 456 | --------------------------------------------------------------------------------