From cb94feb93206f228fd281968dbed681e1ad9a407 Mon Sep 17 00:00:00 2001 From: DerelictDrone <57756830+DerelictDrone@users.noreply.github.com> Date: Fri, 6 Sep 2024 02:16:40 -0500 Subject: [PATCH] ZVM Test Benchmarking (#65) * Basic ZVM Benchmarking * Display per-test benchmark at benchmark > 1, show summed results * Fix trailing comma in print for results * Load from data_static as last resort, new test. * More timing information + ExecutionSteps * Separation of benchmarks, now checks benchmark folder if no files found * Ignore benchmarks for addon to avoid bloat --- addon.json | 1 + .../tests/benchmarks/benchmark_page_bios.lua | 601 ++++++++++++++++++ .../benchmarks/benchmark_zasm_assembler.lua | 492 ++++++++++++++ lua/wire/zvm/tests/execute_from_iobus.lua | 2 +- lua/wire/zvm/zvm_tests.lua | 155 ++++- 5 files changed, 1239 insertions(+), 12 deletions(-) create mode 100644 lua/wire/zvm/tests/benchmarks/benchmark_page_bios.lua create mode 100644 lua/wire/zvm/tests/benchmarks/benchmark_zasm_assembler.lua diff --git a/addon.json b/addon.json index 56443c1..5f7811e 100644 --- a/addon.json +++ b/addon.json @@ -15,5 +15,6 @@ "gitrid.sh", "LICENSE", "wiremod.*" + "benchmark_*" ] } \ No newline at end of file diff --git a/lua/wire/zvm/tests/benchmarks/benchmark_page_bios.lua b/lua/wire/zvm/tests/benchmarks/benchmark_page_bios.lua new file mode 100644 index 0000000..14abedc --- /dev/null +++ b/lua/wire/zvm/tests/benchmarks/benchmark_page_bios.lua @@ -0,0 +1,601 @@ +local Test = {} + + +-- This is a real ZCPU program that sets itself up as a BIOS for a paged memory system +-- It's currently the most complex program that doesn't require emulating hardware that +-- I have available for benchmarking purposes. +Test.Files = { + ["palloc.txt"] = [[ +DATA + +smint: +ALLOC 20*4 +DB psoftware_interrupt_handler,0,os_ptable,160 +smint_end: + +// This needs to set up a stack, so these are the registers required +// to be stored beforehand so as to not destroy anything the user might want. +psoftware_eax: +DB 0 +psoftware_esi: +DB 0 +psoftware_ds: +DB 0 +psoftware_interrupt_handler: + // EAX = interrupt to call, saved. + // Args vary based on interrupt after that point. +CLI + MOV [CS:psoftware_ds],DS // CS is the only register we can trust to be zeroed right now without destroying it first. + MOV DS,0 + MOV [psoftware_eax],EAX + MOV [psoftware_esi],ESI // Because we don't want to destroy DS nor do we want to use it. + XCHG EAX,EDX + CPUGET EDX,41 + MOV [pmem_find_requester_stackless_return],psoftware_got_requester + JMP pmem_find_requester_stackless + psoftware_got_requester: + MOV [pmem_find_requester_stackless_return],0 + CMP ESI,-1 + JNE psoftware_continue + STI + CLERR + // We really don't want this interrupt to go to an unknown ptable, just reset at this point. + INT 0 + psoftware_continue: + MOV [pmem_requester_page],ESI + XCHG EAX,EDX + MOV EAX,[psoftware_eax] + FINT EAX + CMP EAX,(psoftware_interrupt_vector_end-psoftware_interrupt_vector) + JG psoftware_end + CMP EAX,0 + JL psoftware_end + MOV EAX,[EAX:psoftware_interrupt_vector] // get an int + CMP EAX,0 + JE psoftware_end + XCHG ESI,EBX // EBX = Which ptable to store stack for. + SUB EBX,os_ptable_entries+1 + DIV EBX,2 + MOV [psoftware_eax],EAX + MOV EAX,psoftware_stack_setup_finished + JMP setup_os_stack + psoftware_stack_setup_finished: + MOV EAX,[psoftware_eax] + XCHG ESI,EBX // Put EBX back, then we return ESI to original state. + MOV ESI,[psoftware_esi] + CALL EAX + XCHG ESI,EBX // Store EBX in ESI, it'll be restored soon. + MOV EBX,[pmem_requester_page] + SUB EBX,os_program_ptables+1 + DIV EBX,2 + MOV EAX,psoftware_program_stack_restored + JMP setup_program_stack // We are now back to being in the program stack. + psoftware_program_stack_restored: + XCHG ESI,EBX // Restore EBX, ESI gets restored properly in psoftware_end + psoftware_end: + MOV EAX,[pmem_requester_page] + MOV [pmem_requester_page],[EAX] // Deref this + MUL [pmem_requester_page],128 + MOV EAX,[psoftware_eax] + MOV ESI,[psoftware_esi] + MOV DS,[psoftware_ds] +STI +CLERR +IRETP [CS:pmem_requester_page] + +psoftware_interrupt_vector: +DB palloc,pfree,pspawn//,pexit +ALLOC 28 // 28 free entries. +psoftware_interrupt_vector_end: + +setup_os_stack_ebx: +DB 0 + +setup_os_stack: + // EAX = Ptr to return to, because we can't *call* this if we're switching stacks now can we? + // EBX = Which program to store this stack's state for. + MOV [setup_os_stack_ebx],EBX + MUL EBX,3 // Size of stack metadata + MOV [EBX:os_program_stacks],SS + INC EBX + MOV [EBX:os_program_stacks],ESP + INC EBX + CPUGET [EBX:os_program_stacks],9 // Stack size + MOV EBX,[setup_os_stack_ebx] + MOV SS,os_stack + CPUSET 9,(os_stack_end-os_stack) + MOV ESP,[os_sp] +JMP EAX + +setup_program_stack: + // EAX = Ptr to return to + // EBX = Which program to load stack information for. + MOV [os_sp],ESP + MOV [setup_os_stack_ebx],EBX + MUL EBX,3 + MOV SS,[EBX:os_program_stacks] + INC EBX + MOV ESP,[EBX:os_program_stacks] + INC EBX + CPUSET 9,[EBX:os_program_stacks] + MOV EBX,[setup_os_stack_ebx] +JMP EAX + +check_page_allocated: + // ESI = PTR to page + INC ESI + CPUSET 25,[ESI] + DEC ESI +RET + +page_in_ptable: + PUSH ESI + // EDX = target page + // ESI = ptable ptr + // ECX = entries to check + page_in_ptable_loop: + INC ESI + CMP [ESI],EDX + INC ESI + JE page_in_ptable_break + LOOP page_in_ptable_loop + page_in_ptable_break: + POP ESI +RET + +pmem_find_requester: + // Provide EDX as PPAGE / CPUGET EDX,41 before call + // Destroys ESI. + MOV ESI,os_ptable_end-2 +pmem_find_requester_loop: + CALL check_page_allocated + JE pmem_skip_ptable + CNZ page_in_ptable // if allocated, check it out + JE pmem_find_requester_break // Found. + pmem_skip_ptable: + CMP ESI,os_program_ptables-2 // Range check + JE pmem_find_requester_fail + SUB ESI,2 +JMP pmem_find_requester_loop + +pmem_find_requester_fail: + MOV ESI,-2 +pmem_find_requester_break: + INC ESI +RET + +page_in_ptable_stackless_esi: +DB 0 +page_in_ptable_stackless: + MOV [page_in_ptable_stackless_esi],ESI + // EDX = target page + // ESI = ptable ptr + // ECX = entries to check + page_in_ptable_stackless_loop: + INC ESI + CMP [ESI],EDX + INC ESI + JE page_in_ptable_stackless_break + LOOP page_in_ptable_stackless_loop + page_in_ptable_stackless_break: + MOV ESI,[page_in_ptable_stackless_esi] +JMP pmem_find_requester_stackless_page_in_ptable_return + +pmem_find_requester_stackless_return: +DB 0 + +pmem_find_requester_stackless: + // Provide EDX as PPAGE / CPUGET EDX,41 before call + // Destroys ESI. + MOV ESI,os_ptable_end-2 +pmem_find_requester_loop_stackless: + INC ESI + CPUSET 25,[ESI] + DEC ESI + JE pmem_stackless_skip_ptable + JNZ page_in_ptable_stackless // if allocated, check it out + pmem_find_requester_stackless_page_in_ptable_return: + JE pmem_find_requester_stackless_break // Found. + pmem_stackless_skip_ptable: + CMP ESI,os_program_ptables-2 // Range check + JE pmem_find_requester_stackless_fail + SUB ESI,2 +JMP pmem_find_requester_loop_stackless + +pmem_find_requester_stackless_fail: + MOV ESI,-2 +pmem_find_requester_stackless_break: + INC ESI +JMP [pmem_find_requester_stackless_return] + +pmem_requester_page: +DB 0 + +palloc: + // EBX = number of pages to allocate + // EBX will be returned as ptr to allocated region + PUSH EAX + PUSH EDX + PUSH ECX + PUSH ESI + PUSH EDI + MOV ECX,[pmem_requester_page] + SUB ECX,os_ptable_entries + MUL ECX,64 // Mapped address for where we're storing the (aligned to page boundaries) ptable + MOV EDI,DS + MOV EDX,EBX + MOV DS,free_page_stack + palloc_get_freepages: + MOV EAX,[-1] // get from stack using memory stack ptr + MOV EAX,[EAX] + PUSH EAX + INC [-1] // increment as this is a pop operation + LOOPB palloc_get_freepages + MOV EBX,EDX + MOV EAX,126 + MOV EDX,0 // counter for how many contiguous free mappings we've found + MOV DS,ECX // should be start of the target ptable + palloc_find_unmapped_section: + CMP [EAX],0 + INC EDX + JE palloc_unmapped_page + MOV EDX,0 + palloc_unmapped_page: + CMP EDX,EBX + JE palloc_find_unmapped_section_break // Found a contiguous section. + SUB EAX,2 + CMP EAX,0 + JE palloc_fail_allocate + JMP palloc_find_unmapped_section + palloc_fail_allocate: + MOV DS,EBX + MOV EBX,-1 + JMP palloc_end // failed to find a section large enough. + palloc_find_unmapped_section_break: + MOV EDI,EAX // start of contiguous section. + palloc_map_pages: + MOV ESI,[EAX] + BOR ESI,2 // Set remapped bit + BAND ESI,65202 // Mask for full run level and the remapped bit. + MOV [EAX],ESI + INC EAX + POP [EAX] // Map page from stack entry + INC EAX + LOOPB palloc_map_pages + MOV DS,EBX + MOV EBX,EDI + MUL EBX,64 +palloc_end: + POP EDI + POP ESI + POP ECX + POP EDX + POP EAX +RET + +// EBX = address to be freed(divisible by 128 only) +// ECX = number of pages to be freed +pfree: + PUSH ESI + PUSH DS + PUSH ECX + MOV ESI,[pmem_requester_page] + SUB ESI,os_ptable_entries + MUL ESI,64 + MOV DS,ESI // Now targeting requesting ptable + DIV EBX,64 // Get the byte for the permissions mask + POP ECX + MOV EDX,ESP + pfree_loop: + BIT [EBX],4 // Check if page shouldn't be returned to stack on free + BOR [EBX],1 // Set disabled bit + BAND [EBX],65201 // Permissions mask with the disabled bit + INC EBX + JNZ pfree_destroy // Destroy the page without returning it to stack if bit 4(32) enabled. + CMP [EBX],0 // Don't pollute the freed pages with unmapped ones. + JE pfree_skip + PUSH [EBX] + pfree_destroy: + MOV [EBX],0 + pfree_skip: + INC EBX + LOOP pfree_loop + MOV DS,free_page_stack + MOV ECX,EDX + MOV EDX,ESP + SUB ECX,EDX // ECX is now oldstack-newstack + CMP ECX,0 + JE pfree_end + pfree_push_freedpages: + MOV EDX,[-1] // get from stack using memory stack ptr + POP [EDX] + DEC [-1] // decrement as this is a push operation + LOOP pfree_push_freedpages +pfree_end: + POP DS + POP ESI +RET + +pspawn: +CLI + // EDX = Spawn and start, or just spawn? + // ESI = ptr to buffer to load as program + // EBX = program size to load + // ECX = program stack size given(ceils to higher page) + PUSH EBP // 1 + MOV EBP,EDX // Save EBP as the switch, it'll later take the physical page for the spawned program if nonzero. + PUSH EDX // 2 + CPUGET EDX,41 + PUSH ESI // 3 + PUSH ECX // 4 + PUSH EBX // 5 + PUSH DS // 6 + PUSH KS // 7 + PUSH LS // 8 + // Find a free ptable in os_program_ptables + MOV EDX,os_ptable_end-os_program_ptables + MOV DS,os_program_ptables + pspawn_find_free_ptable_loop: + CMP [EDX],0 + JE pspawn_find_free_ptable_break + DEC EDX // double decrement with loopd + this + LOOPD pspawn_find_free_ptable_loop + JMP pspawn_exit + pspawn_find_free_ptable_break: + // EDX is now a ptr to a free table local to os_program_ptables + PUSH EAX // 9 + // Pop a page off of the free page stack to use + MOV KS,free_page_stack + INC [KS:-1] + MOV EAX,[KS:-1] + MOV DS,os_program_ptables + MOV [EDX],2 // Set as remapped + INC EDX + MOV [EDX],[KS:EAX] // Put page as ptable + // Set up the stack values for this entry. + PUSH EDX + DEC EDX + MUL EDX,1.5 // DIV by 2 MUL by 3, for converting a 2 wide index to a 3 wide index to account for stack entry size. + MOV [EDX:os_program_stacks],EBX // SS is end of requested program memory. + INC EDX + SUB ECX,2 + MOV [EDX:os_program_stacks],ECX // ESP is ECX-1 if empty but we need to set it to ECX-3 to pop CS,IP + ADD ECX,2 + INC EDX + MOV [EDX:os_program_stacks],ECX // ESZ is ECX + // We need to find a way to write 0,0 to the stack but I'll do that later. + POP EDX // EDX is now restored to original value. + CPUSET 25,EBP + JZ pspawn_skip_storage + MOV EBP,EDX + ADD EBP,os_program_ptables + pspawn_skip_storage: + POP EAX // 8 + DEC EDX + ADD EDX,(os_program_ptables-os_ptable_entries) + MUL EDX,64 // Get virtual address to mapped page table so we can set it up. + MOV DS,0 + PUSH EDX // 9 + MOV [EDX],1 // Page permission of default page now disabled + INC EDX + MOV [EDX],0 // No mapped index. + INC EDX + // Now we need to map enough pages to fit both the stack size, and the program's bytes. + PUSH EBX // 10 + ADD EBX,ECX + DIV EBX,128 + FCEIL EBX + // Now EBX is the number of pages required to fit all of the program's requested memory. + // Begin page setup. + PUSH EAX // 11 + PUSH EDX // 12 + pspawn_map_program_pages: + MOV [EDX],2 // Remapped. + INC EDX + INC [KS:-1] + MOV EAX,[KS:-1] + MOV R5,EAX + MOV R6,[KS:EAX] + MOV [EDX],[KS:EAX] + INC EDX + LOOPB pspawn_map_program_pages + // We need to zero all of the other pages in here actually. + // Do this later maybe? + POP LS // 11 LS is now the spawned ptable + POP EAX // 10 + POP EBX // 9 + // Now we need to copy the program to the pages from the pointer provided. + MOV R5,LS + PUSH ESI // 10 + MOV EDX,[pmem_requester_page] // Get the requester page, this is raw right now. + // Requester page is the absolute address of the page's mapping in os_program_ptables. + DIV ESI,128 // Which page entry to check for this virtual address. + FINT ESI + INC ESI // 1 index this, to account for default page. + MUL ESI,2 + SUB EDX,os_ptable_entries+1 + MUL EDX,64 // Get address to the remapping of the requester ptable. + PUSH EDX // 11 + ADD EDX,ESI + MOV KS,EDX // we don't need the free page stack anymore, KS will be the requester ptables offset + POP EDX // 10 + POP ESI // 9 + POP EDX // 8 + PUSH EDI // 9 + MOV EDI,0 + MOD ESI,128 + // Load page mapping. + MOV [os_dtransfer_esi+1],[KS:1] + ADD KS,2 + MOV [os_dtransfer_edi+1],[LS:1] + ADD LS,2 + pspawn_copy_loop: + MOV [EDI:((os_dtransfer_edi-os_ptable_entries)*64], // edi page + [ESI:((os_dtransfer_esi)-os_ptable_entries)*64] // esi page + INC ESI + INC EDI + CMP ESI,128 + JE pspawn_reset_esi + pspawn_return_esi: + CMP EDI,128 + JE pspawn_reset_edi + pspawn_return_edi: + LOOPB pspawn_copy_loop + JMP pspawn_finished_copy + pspawn_reset_esi: + MOV ESI,0 + // shift source page + MOV [os_dtransfer_esi+1],[KS:1] + ADD KS,2 + JMP pspawn_return_esi + pspawn_reset_edi: + MOV EDI,0 + // shift destination page + MOV [os_dtransfer_edi+1],[LS:1] + ADD LS,2 + JMP pspawn_return_edi + pspawn_finished_copy: + POP EDI // 8 + CPUSET 25,EBP + JZ pspawn_skip_set + MOV [pmem_requester_page],EBP // Now we'll jump to the spawned program after. + pspawn_skip_set: +pspawn_exit: + POP LS // 7 + POP KS // 6 + POP DS // 5 + POP EBX // 4 + POP ECX // 3 + POP ESI // 2 + POP EDX // 1 + POP EBP // 0 +RET +// 4 and up are for remapping to other ptables +os_ptable: +DB 0,0 // Default page, permissions here are used if page < 0 or > PTBE. Not disabled so mem and port access can happen from OS. +os_ptable_entries: +DB 0,0 // 0-127 1 +DB 0,0 // 128-255 2 +DB 0,0 // 256-383 3 +DB 0,0 // 384-511 4 +DB 0,0 // 512-639 5 +DB 0,0 // 640-767 6 +DB 0,0 // 768-895 7 +DB 0,0 // 896-1023 8 +DB 0,0 // 1024-1151 9 +DB 0,0 // 1152-1279 10 +os_program_ptables: +DB 2,32 // 1280-1407 default map as 4096 +DB 0,0 // 1408-1535 +DB 0,0 // 1536-1663 +DB 0,0 // 1664-1791 +os_ptable_end: +os_dtransfer_map: // For setting up buffer/entire page copies. +os_dtransfer_esi: // Source page +DB 2,0 // 1792-1919 +os_dtransfer_edi: // Destination page +DB 2,0 // 1920-2047 + +os_program_stacks: +ALLOC 4*3 +os_program_stacks_end: + +// Stack for currently free pages. +free_page_sp: +DB 63 +free_page_stack: +ALLOC 64 +end_free_page_stack: + +// Stack for OS operations. +os_sp: +DB 15 +os_stack: +ALLOC 16 +os_stack_end: + +CODE +CPUSET 37,os_ptable +//CPUSET 38,(os_ptable_end-os_ptable)/2 +CPUSET 38,64 // 64 page entries, so we won't have to switch the size when we enter a program. +CPUSET 52,(smint_end-smint)/4 +MOV ESP,(os_stack_end-os_stack)-1 +MOV SS,(os_stack) +CPUSET 9,(os_stack_end-os_stack) +LIDTR smint +STEF +STM +STI + + +// Get the free pages for popping. +MOV EDI,(free_space/128) +MOV DS,free_page_stack +generate_free_pages: + MOV EDX,[-1] + MOV [EDX],EDI + INC EDI + DEC [-1] + CMP [-1],0 + MOV PORT0,EDX +JG generate_free_pages +MOV DS,0 +MOV EDI,0 +MOV EDX,0 +PUSH 0 // CS +PUSH 0 // IP +IRETP 4096 +pre_free_space: +ORG 1152 +free_space: +ORG 4096 + + ]], + ["palloc_spawn_example.txt"] = [[ +#include +ptable: +DB 0,0 +DB 2,(program/128) +z: +DB 2,(program2/128) +ALLOC 122 + +OFFSET -4224 +program: +MOV ESI,program2 +MOV EBX,128 // Code size +MOV ECX,128 // Stack size(allocates but doesn't set up for you) +MOV EDX,1 // 1 = Run spawned program immediately after, 0 = Load but don't run spawned program immediately. +MOV ESI,program2 +MOV EAX,2 +INT 20 // PSPAWN +p1_loop: +INC R5 +JMP p1_loop +ALLOC 102 +program2: +INC R4 +MOV R6,ESP +MOV R7,SS +JMP 0 +]] +} + +function Test.Run(CPU,TestSuite) + CPU.RAMSize = 32768 + CPU.ROMSize = 32768 + CPU.Frequency = 1e6 + TestSuite:Deploy(CPU,TestSuite:LoadFile("palloc_spawn_example.txt"),Test.CompileError) + CPU.Clk = 1 + for i = 0, 1024*1024 do + CPU:RunStep() + end + -- On false, will cause test to fail with message + assert(not CPU.VMStopped,"VM Stopped at end of execution, likely an unhandled interrupt caused an error.") +end + +function Test.CompileError(msg) + assert(false,"compile time error: " .. msg) +end + +return Test \ No newline at end of file diff --git a/lua/wire/zvm/tests/benchmarks/benchmark_zasm_assembler.lua b/lua/wire/zvm/tests/benchmarks/benchmark_zasm_assembler.lua new file mode 100644 index 0000000..05c127f --- /dev/null +++ b/lua/wire/zvm/tests/benchmarks/benchmark_zasm_assembler.lua @@ -0,0 +1,492 @@ +local Test = {} + + +-- This is a real ZCPU program written in HLZASM syntax that compiles human readable code to ZASM bytecode. +-- Uses multiple lookup tables including alphabetical sorting to reduce search time for name => instruction. +-- +-- Written by Huh in the Wiremod Discord +Test.Files = { + ["assembler.txt"] = [[ +#pragma CRT ZCRT +#define inst_lookup_K 0 +#define inst_lookup_U 0 +#define inst_lookup_V 0 +#define inst_lookup_Y 0 +#define inst_lookup_Z 0 + +void main() { + char textToAssemble = "MOV EAX, EBX" + float start + float end + timer start + assemble(textToAssemble, ASMOutput) + timer end + port0 = end - start +} + +void assemble(char asm, float output) { + char word = asm + float k = 0 + while(word[0]) { + word = getNextWord(asm) + float inst = isInstruction(asm) + asm = word + 1 + if(inst[0] == 999) { + asm = handleDB(asm, &k, output) + word = asm + continue + } + float reg + float i = inst[inst[1] + 3] + float ic = i + float rm1 = 0 + float rm2 = 0 + float v + output[k++] = inst[0] + while(i) { + word = getNextWord(asm) + reg = isRegister(asm) + if(i == 2) { + if(reg) { + rm1 = reg[0] + } else if(asm[0] == 35) { + reg = isRegister(asm + 1) + rm1 = reg[0] + 16 + //k++ + } + //k++ + asm = word + 2 + } else { + v = k + if(reg) { + rm2 = reg[0] + } else if(asm[0] == 35) { + reg = isRegister(asm + 1) + rm2 = reg[0] + 16 + //k++ + } else { + rm2 = 0 + output[++k] = atof(asm) + } + if(ic == 1) { + rm1 = rm2 + rm2 = 0 + } + output[v] = rm1 + (10000 * rm2) + k++ + asm = word + 1 + } + i-- + } + } + //port0 = word[0] +} + +float atof(char str) { + zap R0 R1 R2 R3 R4 + register float res = 0; + register float i; + register float cur = str[0] + for (i = 0; cur && (cur != 32) && (cur != 44) && (cur != 46); i++) { + res = res * 10 + cur - '0'; + cur = str[i + 1] + } + if(str[i] == 46) { + register float res2 = 0 + register float k = 0 + register float p = 10 + cur = str[++i] + for (i = i; cur && (cur != 32) && (cur != 44); ++i) { + res2 = res2 * 10 + cur - '0'; + k++ + cur = str[i] + } + FPWR p, k + res += res2 / p + } + return res; +} + +char getNextWord(char asm) { + register float i = 0 + register float cur = asm[i] + while(cur && (cur != 32) && (cur != 44)) { + i++ + cur = asm[i] + } + return i + asm +} + +char handleDB(char asm, float* k, char output) { + float i = 0 + float p = asm + while(p[0] && (p[0] != 32)) { + p = getNextWord(asm) + output[k[0] + i] = atof(asm) + asm = p + 1 + i++ + } + k[0] += i + if(p[0]) { + p++ + } + return p +} + +float isRegister(char* x) { + //preserve ECX + zap R0 R1 R2 R3 R4 R5 + register float i = 0; + register float k = 0; + while(i < 20) { + register float y = 0; + register float v = 0; + register char cur = x[v]; + while(cur && (cur != 32) && (cur != 44)) { + if(cur == registers[k + 2 + v]) { + y++ + } else { + y-- + } + v++ + cur = x[v]; + } + if(y == registers[k + 1]) { + break + } + k += registers[k + 1] + 3 + i++ + } + + if(i == 20) { + return 0 + } else { + return k + registers + } +} + +float isInstruction(char* x) { + //preserve ECX + zap R0 R1 R2 R3 R4 R5 + register float i = 0; + register float k = 0; + register float lookup = x[0] - 65 + optable + lookup = lookup[0] + register float maxItems = lookup[0] + maxItems++ + lookup++ + while(i < maxItems) { + register float y = 0; + register float v = 0; + register char cur = x[v]; + while(cur && (cur != 32) && (cur != 44)) { + if(cur == lookup[k + 2 + v]) { + y++ + } else { + y-- + } + v++ + cur = x[v]; + } + if(y == lookup[k + 1]) { + break + } + k += lookup[k + 1] + 4 + i++ + } + + if(i == maxItems) { + return 0 + } else { + return k + lookup + } +} + +registers: // 1: Opcode, 2: Register name length, 3: Register name +DB 1, 3, "EAX",0 +DB 2, 3, "EBX",0 +DB 3, 3, "ECX",0 +DB 4, 3, "EDX",0 +DB 5, 3, "ESI",0 +DB 6, 3, "EDI",0 +DB 7, 3, "ESP",0 +DB 8, 3, "EBP",0 +DB 9, 2, "CS",0 +DB 10, 2, "SS",0 +DB 11, 2, "DS",0 +DB 12, 2, "ES",0 +DB 13, 2, "GS",0 +DB 14, 2, "FS",0 +DB 15, 2, "KS",0 +DB 16, 2, "LS",0 +DB 1000, 5, "PORT0",0 +DB 1001, 5, "PORT1",0 +DB 1002, 5, "PORT2",0 +DB 1003, 5, "PORT3",0 + +optable: +DB +inst_lookup_A, +inst_lookup_B, +inst_lookup_C, +inst_lookup_D, +inst_lookup_E, +inst_lookup_F, +inst_lookup_G, +inst_lookup_H, +inst_lookup_I, +inst_lookup_J, +inst_lookup_K, +inst_lookup_L, +inst_lookup_M, +inst_lookup_N, +inst_lookup_O, +inst_lookup_P, +inst_lookup_Q, +inst_lookup_R, +inst_lookup_S, +inst_lookup_T, +inst_lookup_U, +inst_lookup_V, +inst_lookup_W, +inst_lookup_X, +inst_lookup_Y, +inst_lookup_Z +inst_lookup_A: +db 2 +db 10, 3, "ADD",0, 2 +db 50, 3, "AND",0, 2 +inst_lookup_B: +db 8 +db 32, 4, "BNOT",0, 1 +db 60, 3, "BIT",0, 2 +db 64, 4, "BAND",0, 2 +db 65, 3, "BOR",0, 2 +db 66, 4, "BXOR",0, 2 +db 67, 4, "BSHL",0, 2 +db 68, 4, "BSHR",0, 2 +db 127, 5, "BLOCK",0, 2 +inst_lookup_C: +db 28 +db 8, 5, "CPUID",0, 1 +db 15, 3, "CMP",0, 2 +db 29, 3, "CPG",0, 1 +db 31, 4, "CALL",0, 1 +db 43, 3, "CLI",0, 0 +db 45, 3, "CLP",0, 0 +db 49, 4, "CLEF",0, 0 +db 62, 4, "CBIT",0, 2 +db 71, 3, "CNE",0, 1 +db 71, 3, "CNZ",0, 1 +db 73, 2, "CG",0, 1 +db 73, 4, "CNLE",0, 1 +db 74, 3, "CGE",0, 1 +db 74, 3, "CNL",0, 1 +db 75, 2, "CL",0, 1 +db 75, 4, "CNGE",0, 1 +db 76, 3, "CLE",0, 1 +db 76, 3, "CNG",0, 1 +db 77, 2, "CE",0, 1 +db 77, 2, "CZ",0, 1 +db 89, 5, "CALLF",0, 2 +db 119, 3, "CLM",0, 0 +db 120, 6, "CPUGET",0, 2 +db 121, 6, "CPUSET",0, 2 +db 123, 3, "CPP",0, 2 +db 125, 3, "CRL",0, 2 +db 128, 6, "CMPAND",0, 2 +db 129, 5, "CMPOR",0, 2 +db 151, 5, "CLERR",0, 0 +inst_lookup_D: +db 2 +db 13, 3, "DIV",0, 2 +db 21, 3, "DEC",0, 1 +db 999, 2, "DB",0, 0 +inst_lookup_E: +db 7 +db 70, 6, "EXTINT",0, 1 +db 95, 4, "ERPG",0, 1 +db 110, 6, "EXTRET",0, 0 +db 135, 5, "ENTER",0, 1 +db 137, 7, "EXTRETP",0, 1 +db 140, 7, "EXTRETA",0, 0 +db 141, 8, "EXTRETPA",0, 1 +inst_lookup_F: +db 21 +db 33, 4, "FINT",0, 1 +db 34, 4, "FRND",0, 1 +db 35, 5, "FFRAC",0, 1 +db 36, 4, "FINV",0, 1 +db 38, 4, "FSHL",0, 1 +db 39, 4, "FSHR",0, 1 +db 53, 4, "FSIN",0, 2 +db 54, 4, "FCOS",0, 2 +db 55, 4, "FTAN",0, 2 +db 56, 5, "FASIN",0, 2 +db 57, 5, "FACOS",0, 2 +db 58, 5, "FATAN",0, 2 +db 80, 4, "FPWR",0, 2 +db 82, 4, "FLOG",0, 2 +db 82, 3, "FLN",0, 2 +db 83, 6, "FLOG10",0, 2 +db 86, 4, "FABS",0, 2 +db 87, 4, "FSGN",0, 2 +db 88, 4, "FEXP",0, 2 +db 90, 3, "FPI",0, 1 +db 91, 2, "FE",0, 1 +db 94, 5, "FCEIL",0, 1 +inst_lookup_G: +db 1 +db 132, 4, "GMAP",0, 2 +inst_lookup_H: +db 1 +db 37, 4, "HALT",0, 1 +inst_lookup_I: +db 6 +db 20, 3, "INC",0, 1 +db 41, 4, "IRET",0, 0 +db 84, 2, "IN",0, 2 +db 92, 3, "INT",0, 1 +db 111, 4, "IDLE",0, 0 +db 136, 5, "IRETP",0, 1 +inst_lookup_J: +db 26 +db 1, 3, "JNE",0, 1 +db 1, 3, "JNZ",0, 1 +db 2, 3, "JMP",0, 1 +db 3, 2, "JG",0, 1 +db 3, 4, "JNLE",0, 1 +db 4, 3, "JGE",0, 1 +db 4, 3, "JNL",0, 1 +db 5, 2, "JL",0, 1 +db 5, 4, "JNGE",0, 1 +db 6, 3, "JLE",0, 1 +db 6, 3, "JNG",0, 1 +db 7, 2, "JE",0, 1 +db 7, 2, "JZ",0, 1 +db 69, 4, "JMPF",0, 2 +db 101, 4, "JNER",0, 1 +db 101, 4, "JNZR",0, 1 +db 102, 4, "JMPR",0, 1 +db 103, 3, "JGR",0, 1 +db 103, 5, "JNLER",0, 1 +db 104, 4, "JGER",0, 1 +db 104, 4, "JNLR",0, 1 +db 105, 3, "JLR",0, 1 +db 105, 5, "JNGER",0, 1 +db 106, 4, "JLER",0, 1 +db 106, 4, "JNGR",0, 1 +db 107, 3, "JER",0, 1 +db 107, 3, "JZR",0, 1 +inst_lookup_L: +db 13 +db 24, 4, "LOOP",0, 1 +db 24, 5, "LOOPC",0, 1 +db 25, 5, "LOOPA",0, 1 +db 26, 5, "LOOPB",0, 1 +db 27, 5, "LOOPD",0, 1 +db 50, 4, "LAND",0, 2 +db 51, 3, "LOR",0, 2 +db 52, 4, "LXOR",0, 2 +db 99, 5, "LIDTR",0, 1 +db 108, 4, "LNEG",0, 1 +db 108, 4, "LNOT",0, 1 +db 117, 5, "LEAVE",0, 0 +db 126, 3, "LEA",0, 2 +inst_lookup_M: +db 8 +db 12, 3, "MUL",0, 2 +db 14, 3, "MOV",0, 2 +db 18, 3, "MIN",0, 2 +db 19, 3, "MAX",0, 2 +db 59, 3, "MOD",0, 2 +db 78, 5, "MCOPY",0, 1 +db 79, 5, "MXCHG",0, 1 +db 130, 6, "MSHIFT",0, 2 +inst_lookup_N: +db 4 +db 22, 3, "NEG",0, 1 +db 70, 6, "NMIINT",0, 1 +db 110, 6, "NMIRET",0, 0 +db 112, 3, "NOP",0, 0 +inst_lookup_O: +db 2 +db 51, 2, "OR",0, 2 +db 85, 3, "OUT",0, 2 +inst_lookup_P: +db 4 +db 9, 4, "PUSH",0, 1 +db 30, 3, "POP",0, 1 +db 114, 5, "PUSHA",0, 0 +db 115, 4, "POPA",0, 0 +inst_lookup_Q: +db 2 +db 152, 6, "QUOCMP",0, 0 +db 153, 8, "QUOTIMER",0, 1 +inst_lookup_R: +db 12 +db 0, 8, "RESERVED",0, 0 +db 16, 2, "RD",0, 2 +db 23, 4, "RAND",0, 1 +db 34, 3, "RND",0, 1 +db 40, 3, "RET",0, 0 +db 46, 8, "RESERVED",0, 0 +db 47, 4, "RETF",0, 0 +db 72, 8, "RESERVED",0, 1 +db 97, 4, "RDPG",0, 1 +db 100, 8, "RESERVED",0, 1 +db 109, 8, "RESERVED",0, 1 +db 113, 8, "RESERVED",0, 0 +db 133, 6, "RSTACK",0, 2 +inst_lookup_S: +db 12 +db 11, 3, "SUB",0, 2 +db 28, 3, "SPG",0, 1 +db 42, 3, "STI",0, 0 +db 44, 3, "STP",0, 0 +db 48, 4, "STEF",0, 0 +db 61, 4, "SBIT",0, 2 +db 116, 4, "STD2",0, 0 +db 118, 3, "STM",0, 0 +db 122, 3, "SPP",0, 2 +db 124, 3, "SRL",0, 2 +db 131, 4, "SMAP",0, 2 +db 134, 6, "SSTACK",0, 2 +db 150, 5, "STERR",0, 2 +inst_lookup_T: +db 3 +db 63, 4, "TBIT",0, 2 +db 93, 3, "TPG",0, 1 +db 98, 5, "TIMER",0, 1 +inst_lookup_W: +db 2 +db 17, 2, "WD",0, 2 +db 96, 4, "WRPG",0, 1 +inst_lookup_X: +db 2 +db 52, 3, "XOR",0, 2 +db 81, 4, "XCHG",0, 2 + +ASMOutput: +]] +} + +function Test.Run(CPU,TestSuite) + CPU.RAMSize = 131072 + CPU.ROMSize = 131072 + CPU.Frequency = 1e6 + TestSuite:Deploy(CPU,TestSuite:LoadFile("assembler.txt"),Test.CompileError) + CPU.Clk = 1 + for i = 0, 65536 do + CPU:RunStep() + end + -- On false, will cause test to fail with message + -- Expecting LINT 1 aka HLZASM main returned. + assert(not CPU.VMStopped or CPU.LINT == 1,"VM Stopped at end of execution, likely an unhandled interrupt caused an error. LINT = "..CPU.LINT.." LADD ="..CPU.LADD) +end + +function Test.CompileError(msg) + assert(false,"compile time error: " .. msg) +end + +return Test \ No newline at end of file diff --git a/lua/wire/zvm/tests/execute_from_iobus.lua b/lua/wire/zvm/tests/execute_from_iobus.lua index bb5eaf6..38f1ccb 100644 --- a/lua/wire/zvm/tests/execute_from_iobus.lua +++ b/lua/wire/zvm/tests/execute_from_iobus.lua @@ -1,7 +1,7 @@ local Test = {} function Test.Run(CPU,TestSuite) - TestSuite.Compile("MOV R0,6 ADD R0,R0 MUL R0,2", "internal", nil, Test.CompileError) + TestSuite.Compile("MOV R0,6 ADD R0,R0 MUL R0,2 DB 0", "internal", nil, Test.CompileError) -- end result of the above code should be R0 = 24 local buff = TestSuite.GetCompileBuffer() local IOBus = TestSuite.CreateVirtualIOBus(#buff + 1) -- create an IOBus large enough to hold this code diff --git a/lua/wire/zvm/zvm_tests.lua b/lua/wire/zvm/zvm_tests.lua index d9b81db..aeb5fc3 100644 --- a/lua/wire/zvm/zvm_tests.lua +++ b/lua/wire/zvm/zvm_tests.lua @@ -16,19 +16,27 @@ ZVMTestSuite = { TestFiles = {}, TestQueue = {}, TestStatuses = {}, + Benchmarks = {}, + BenchmarksByTest = {}, Warnings = 0, CurrentWarnings = 0 } local testDirectory = "wire/zvm/tests" +local benchmarkDirectory = testDirectory.."/benchmarks" function ZVMTestSuite.CMDRun(_, _, _, names) ZVMTestSuite.Warnings = 0 ZVMTestSuite.TestFiles = {} + local subdirectory = "" for filename in string.gmatch(names, "[^,]+") do - local files = file.Find("lua/" .. testDirectory .. "/" .. filename .. ".lua", "GAME") + local files = file.Find("lua/" .. testDirectory .. "/" .. filename .. ".lua", "GAME") + if #files == 0 then + files = file.Find("lua/" .. benchmarkDirectory .. "/" .. filename .. ".lua", "GAME") + subdirectory = "/benchmarks/" + end for _, i in ipairs(files) do - ZVMTestSuite.TestFiles[#ZVMTestSuite.TestFiles+1] = i + ZVMTestSuite.TestFiles[#ZVMTestSuite.TestFiles+1] = subdirectory..i end end if #ZVMTestSuite.TestFiles == 0 and names ~= nil then @@ -52,6 +60,9 @@ end function ZVMTestSuite.StartTesting() ZVMTestSuite.TestQueue = {} ZVMTestSuite.TestStatuses = {} + ZVMTestSuite.Benchmarks = {} + ZVMTestSuite.BenchmarksByTest = {} + ZVMTestSuite.StartTime = os.clock() for ind, i in ipairs(ZVMTestSuite.TestFiles) do -- copy with reversed indexes so we can use cheap popping ZVMTestSuite.TestQueue[(#ZVMTestSuite.TestFiles)+1-ind] = i end @@ -66,14 +77,21 @@ function ZVMTestSuite.FinishTest(fail) else finalFail = fail end + local prevTestIndex = #ZVMTestSuite.TestQueue + local prevTestName = ZVMTestSuite.TestQueue[prevTestIndex] if ZVMTestSuite.CurrentWarnings > 0 then - print("Compiler Warnings from " .. ZVMTestSuite.TestQueue[#ZVMTestSuite.TestQueue] .. ": " .. ZVMTestSuite.CurrentWarnings) + print("Compiler Warnings from " .. ZVMTestSuite.TestQueue[prevTestIndex] .. ": " .. ZVMTestSuite.CurrentWarnings) ZVMTestSuite.CurrentWarnings = 0 end ZVMTestSuite.TestStatuses[#ZVMTestSuite.TestStatuses + 1] = finalFail -- auto fail on return nil - ZVMTestSuite.TestQueue[#ZVMTestSuite.TestQueue] = nil + if ZVMTestSuite.BenchmarksByTest[prevTestName] then + if ZVMTestSuite.BenchmarkConvar:GetInt() > 1 then + PrintTable(ZVMTestSuite.BenchmarksByTest[prevTestName]) + end + end + ZVMTestSuite.TestQueue[prevTestIndex] = nil if #ZVMTestSuite.TestQueue > 0 then - ZVMTestSuite.RunNextTest() + return ZVMTestSuite.RunNextTest() else local passed, failed = 0, 0 for ind,i in ipairs(ZVMTestSuite.TestFiles) do @@ -91,10 +109,36 @@ function ZVMTestSuite.FinishTest(fail) if failed ~= 1 then errormod = "s" end - if ZVMTestSuite.Warnings > 0 then - warnstring = ZVMTestSuite.Warnings .. " Compiler Warnings" + warnstring = ZVMTestSuite.Warnings .. " Compiler Warnings" + -- Sum the benchmarking statistics per each test + if ZVMTestSuite.BenchmarkConvar:GetBool() then + local sumKeys = {"PrecompileStringSize","TotalJitBytecodeSize","PrecompileSteps","Precompiles","FinalCompiledCount","ExecutionTime"} + local topKeys = {"BiggestPrecompileStringSize","BiggestJitBlock","LongestStepExecutionTime"} + local FinalBenchmark = { + PrecompileStringSize = 0, -- Total precompile string size + TotalJitBytecodeSize = 0, -- Total jit bytecode compiled + BiggestPrecompileStringSize = 0, -- The biggest single precompile string + BiggestJitBlock = 0, -- The biggest single block + PrecompileSteps = 0, -- Number of steps in precompile + Precompiles = 0, -- Number of precompile blocks started / finished + FinalCompiledCount = 0, -- Final amount of precompiled blocks at the end of test. + ExecutionTime = 0, -- Total execution time during VM:Step + LongestStepExecutionTime = 0, -- Longest execution time during VM:Step + } + for _,benchmark in ipairs(ZVMTestSuite.Benchmarks) do + for _,key in ipairs(sumKeys) do + FinalBenchmark[key] = FinalBenchmark[key] + benchmark[key] + end + for _,key in ipairs(topKeys) do + FinalBenchmark[key] = math.max(FinalBenchmark[key],benchmark[key]) + end + end + print("\n[Final benchmark stats]\n") + PrintTable(FinalBenchmark) + print("") end print(failed .. " Failed test" .. errormod .. ", " ..passed.. " Passed test" ..passmod.. ", " .. warnstring) + print("Took "..os.clock()-ZVMTestSuite.StartTime.." to execute tests") end end @@ -131,10 +175,16 @@ function ZVMTestSuite.RunNextTest() end function ZVMTestSuite:LoadFile(FileName) - if ZVMTestSuite.VirtualFiles then + if ZVMTestSuite.VirtualFiles and ZVMTestSuite.VirtualFiles[FileName] then return ZVMTestSuite.VirtualFiles[FileName] end - return file.Read("lua/" .. testDirectory .. "/" .. FileName, "GAME") + local testpath = "lua/" .. testDirectory .. "/" .. FileName + local datapath = "data_static/cpuchip/" .. FileName + if file.Exists(testpath,"GAME") then + return file.Read(testpath,"GAME") + elseif file.Exists(datapath,"GAME") then + return file.Read(datapath,"GAME") + end end function ZVMTestSuite.Compile(SourceCode, FileName, SuccessCallback, ErrorCallback, TargetPlatform) @@ -261,6 +311,9 @@ function ZVMTestSuite.AddVirtualFunctions(VM) self.Error = errorcode self.ErrorCallback(errorcode) end + function VM:SignalShutdown() + self.VMStopped = true + end end function ZVMTestSuite.FlashData(VM,data) @@ -288,11 +341,15 @@ function ZVMTestSuite.Run(VM) local CurrentTime = CurTime() local DeltaTime = math.min(1/30,CurrentTime - (VM.PreviousTime or 0)) VM.PreviousTime = CurrentTime - local Cycles = math.max(1,math.floor(VM.Frequency*DeltaTime*0.5)) + if VM.ZVMBenchmark then + -- Benchmark mode seems to consume twice as many cycles so we have to + -- raise the cycle count a bit. + Cycles = Cycles * 2 + end VM.TimerDT = (DeltaTime/Cycles) - while (Cycles > 0) and (VM.Clk) and (not VMStopped) and (VM.Idle == 0) do + while (Cycles > 0) and (VM.Clk) and (not VM.VMStopped) and (VM.Idle == 0) do -- Run VM step local previousTMR = VM.TMR VM:Step() @@ -393,7 +450,83 @@ function ZVMTestSuite.Initialize(VM,Membus,IOBus) VM.VMStopped = false return oldReset(...) end + if ZVMTestSuite.BenchmarkConvar:GetBool() then + if not VM.ZVMBenchmark then + VM.ZVMBenchmark = { + PrecompileStringSize = 0, -- Total precompile string size + TotalJitBytecodeSize = 0, -- Total jit bytecode compiled + BiggestPrecompileStringSize = 0, -- The biggest single precompile string + BiggestJitBlock = 0, -- The biggest single block + PrecompileSteps = 0, -- Number of steps in precompile + Precompiles = 0, -- Number of precompile blocks started / finished + FinalCompiledCount = 0, -- Final amount of precompiled blocks at the end of test. + ExecutionTime = 0, -- Total execution time during VM:Step + LongestStepExecutionTime = 0, -- Longest execution time during VM:Step + ExecutionSteps = 0, -- How many execution steps were performed by this VM + } + + table.insert(ZVMTestSuite.Benchmarks,VM.ZVMBenchmark) + VM.ZVMBenchmark.ProbableOwner = ZVMTestSuite.TestQueue[#ZVMTestSuite.TestQueue] or "Unknown" + if not ZVMTestSuite.BenchmarksByTest[VM.ZVMBenchmark.ProbableOwner] then + ZVMTestSuite.BenchmarksByTest[VM.ZVMBenchmark.ProbableOwner] = {} + end + table.insert(ZVMTestSuite.BenchmarksByTest[VM.ZVMBenchmark.ProbableOwner],VM.ZVMBenchmark) + end + -- Maybe having a "Longest precompile time" or something would be a good idea too but later. + VM.OriginalStep = VM.OriginalStep or VM.Step + function VM:Step(overrideSteps,extraEmitFunction) + local preStep = os.clock() + self:OriginalStep(overrideSteps,extraEmitFunction) + local postStep = os.clock() + local time = postStep-preStep + if time > self.ZVMBenchmark.LongestStepExecutionTime then + self.ZVMBenchmark.LongestStepExecutionTime = time + end + self.ZVMBenchmark.ExecutionTime = self.ZVMBenchmark.ExecutionTime + time + self.ZVMBenchmark.ExecutionSteps = self.ZVMBenchmark.ExecutionSteps + 1 + end + VM.OriginalPrecompile_Step = VM.OriginalPrecompile_Step or VM.Precompile_Step + VM.OriginalPrecompile_Finalize = VM.OriginalPrecompile_Finalize or VM.Precompile_Finalize + VM.OriginalDyn_EndBlock = VM.OriginalDyn_EndBlock or VM.Dyn_EndBlock + function VM:Precompile_Step() + self.ZVMBenchmark.PrecompileSteps = self.ZVMBenchmark.PrecompileSteps + 1 + self:OriginalPrecompile_Step() + end + function VM:Dyn_EndBlock() + local block = self:OriginalDyn_EndBlock() + local blocklen = #block + if blocklen > self.ZVMBenchmark.BiggestPrecompileStringSize then + self.ZVMBenchmark.BiggestPrecompileStringSize = blocklen + end + self.ZVMBenchmark.PrecompileStringSize = self.ZVMBenchmark.PrecompileStringSize + blocklen + return block + end + function VM:Precompile_Finalize() + local precompiledfn = self:OriginalPrecompile_Finalize() + local fninfo = jit.util.funcinfo(precompiledfn) + if fninfo.bytecodes then + if fninfo.bytecodes > self.ZVMBenchmark.BiggestJitBlock then + self.ZVMBenchmark.BiggestJitBlock = fninfo.bytecodes + end + self.ZVMBenchmark.TotalJitBytecodeSize = self.ZVMBenchmark.TotalJitBytecodeSize + fninfo.bytecodes + end + self.ZVMBenchmark.Precompiles = self.ZVMBenchmark.Precompiles + 1 + self.ZVMBenchmark.FinalCompiledCount = self.ZVMBenchmark.FinalCompiledCount + 1 + return precompiledfn + end + -- Just a copy of the one from the ZVM code directly with logging + function VM:InvalidatePrecompileAddress(Address) + if self.IsAddressPrecompiled[Address] then + self.ZVMBenchmark.FinalCompiledCount = self.ZVMBenchmark.FinalCompiledCount - 1 + for k,v in ipairs(self.IsAddressPrecompiled[Address]) do + self.PrecompiledData[v] = nil + self.IsAddressPrecompiled[Address][k] = nil + end + end + end + end end concommand.Add("ZCPU_RUN_TESTS", ZVMTestSuite.CMDRun, nil, "Runs ZCPU Tests, pass a comma delimited list to only run tests with those names\nExample: ZCPU_RUN_TESTS example,file_example\n\nRun without args to run all tests") +ZVMTestSuite.BenchmarkConvar = CreateConVar("ZCPU_TESTS_BENCHMARKING",0,0,"Whether or not to record and report benchmarking information for ZVM Tests",0,2) \ No newline at end of file