SCP Implementation Notes ======================== ============== State Analysis ============== The current trace mechanism works well and is simple. However, detailed tracing substantially slows down execution, presumably due to the formatting overhead and the disc I/O associated with writing the trace log file. Also, it is impractical to have a CPU trace (e.g.) active while waiting for some intermittent problem to occur, due both to the execution slowdown and to the immense size of the resulting trace file. Often, the trace file becomes too large to examine with text browsing tools. A better approach might be to split tracing into data acquisition and interpretation phases. The former would acquire unformatted trace data into a large circular queue, which would then be interpreted and formatted as a post-processing step. If acquisition is simply stuffing bytes into an array for later interpretation, it should reduce the overhead substantially. Ideally, the new overhead would be low enough to allow acquisition to run continuously when a fault is suspected, with the circular queue retaining the last "N" events. Once the fault (e.g., a system failure) occurs, acquisition would be stopped, and the queue examined for the failure occurrence. If the fault halts the CPU, then acquisition will have captured the event at the end of the queue. For other cases, it would be helpful to have a defined trigger event and position that ends acquisition either at the trigger point (if positioned at the end), or after the queue fills until it wraps around and reaches the trigger point (if positioned at the start), or until it fills an additional half of the queue (if positioned at the center). Formatting could be started automatically when the trace buffer is full, or in response to a user command. The latter may be desirable, as a large configured trace buffer size might produce a log file that is too large for examination. Allowing the user to specify the number of trace lines to be interpreted would cut down on this problem and also reduce the user's wait for formatting to complete. -------- Commands -------- // command stream xptr meaning // // Trace_Set_Trigger NULL NULL clear trigger // Trace_Set_Trigger NULL string parse trigger from string // Trace_Show_Trigger file string print trigger on file using string prefix // // Trace_Set_Window NULL NULL clear window // Trace_Set_Window NULL string parse window from string // Trace_Show_Window file string print window on file using string prefix // // Trace_Set_Store NULL NULL clear store states // Trace_Set_Store NULL "" set common states // Trace_Set_Store NULL string parse store states from string // // Trace_Show_List file entry list trace entry on file ----- Commands provided are: - TRACE MEMORY= Allocate or reallocate a trace memory containing entries. If is zero, free the trace memory. Ideally, would allow "K" and "M" suffixes to multiply the count by 1,000 or 1,000,0000. Default is zero (no trace buffer). - TRACE TRIGGER [MODIFY] {NOTHING | =[;...]}[ OR ...] - TRACE TRIGGER NOTHING - TRACE TRIGGER ANYTHING Sets up the trigger event that stops tracing. Acquisition does not occur until a TRACE EXECUTE command is entered. ANYTHING triggers immediately, acquires one trace buffer, and stops. NOTHING never triggers and so acquires trace data continuously until a TRACE HALT command is entered. Trigger sources and values are: Device Source Value Qualifier Value ------ ----------- ----------------- ---------- ------------ CPU INSTRUCTION IGNORE TRAP PARAMETER INTERRUPT PARAMETER IOP (none) MCL ADDRESS DATA CLASS STATUS READ | WRITE SEL (none) MPX (none) CPP (none) ATCD INPUT CHANNEL OUTPUT CHANNEL BREAK CHANNEL OVERRUN CHANNEL ATCC (none) SCMB (none) CLK (none) IMBA (none) DS COMMAND UNIT BLOCK [only for R/W command] SSLC INPUT OUTPUT OVERRUN LINK LP OUTPUT FORMAT MS COMMAND UNIT GIC (none) ADCC INPUT PORT OUTPUT PORT BREAK PORT OVERRUN PORT DC DEVICE PARAMETER BUS TRANSPARENT PARAMETER BUS MA COMMAND PARAMETER UNIT INTERFACE UNIT LPA COMMAND OUTPUT FORMAT RANGE is a list of individual channel numbers separated by commas and ranges of the form -. Commas are used here, rather than the semicolons that are used in specifying multiplexer line order, because = pairs are separated by semicolons. Using them would lead to confusing entries such as TRIGGER ADCC BREAK;PORT=1;2;OVERRUN. MA INTERFACE specifies a bus interface command. PARAMETER specifies a tape command or the parameter for the End command. MA TAPE= is a shortcut for INTERFACE=2;PARAMETER=. ADDRESS triggers when the specified memory address is accessed. The trigger may be further qualified by specifying a DATA value and/or a STATUS value (READ or WRITE). TRAP triggers when the specified trap number occurs. The trigger may be further qualified by specifing the trap parameter number as the DATA value. IRQ triggers when an external interrupt occurs for the specified device. The device number is determined by checking the DEVNO assigned to the named device. Multiple concurrent triggers may be set by specifying additional device types and conditions, either separated by OR (commas cannot be used because may contain one or more commas, e.g., LOAD Q+3,I), or as separate commands. Specifying ANYTHING or NOTHING clears all of the preceding trigger conditions. Default is ANYTHING. - TRACE TRIGGER POSITION={ START | CENTER | END } Sets up the location of the trigger in the trace buffer, which determines the count of additional states acquired after the trigger occurs. START acquires a full trace buffer. CENTER acquires one-half of a trace buffer. END acquires no additional states. Default is START. ( trigger position_is { states_after_start | states_before_end } - TRACE STORE [MODIFY] [{NOTHING | ANYTHING | [;...]}][ OR ...] - TRACE STORE NOTHING Set the specified debug flags in the indicated devices. If the device's list is omitted, then the "usual" debug flags are enabled (as specified by the device). If NOTHING is specified, then all debug flags for the specified device or all devices are cleared. This is just a convenience to permit multiple SET DEBUG= specifications in a single command. States for storage are: Device State Implicit Window ------ ------ -------- ------ CPU INSTR ** Y DATA ** Y FETCH Y REG ** Y OPND ** Y IRQ ** Y PSERV Y IOP DIO ** Y DATA Y MCL DATA ** - IMBUS - SEL PIO ** Y INSTR ** Y CSRW ** Y FETCH Y STATE Y SR Y DATA Y MPX PIO ** Y INSTR ** Y CSRW ** * FETCH Y STATE Y SR Y IOBUS N CPP PIO ** Y INSTR ** Y CMD ** Y SERV * DATA * ATCD CSRW ** * XFER ** Y SERV Y PSERV N IOIRQ ** N IOBUS N ATCC CSRW ** Y XFER ** Y PSERV N IOIRQ ** N IOBUS N SCMB CSRW ** - XFER ** - SERV - IOIRQ - IOBUS - CLK CSRW ** - PSERV - IOIRQ - IOBUS - IMBA CSRW ** - DATA - IOIRQ - IOBUS - IMBRQ - IMBUS - DS CMD ** * INCO ** * CSRW ** * STATE * XFER Y SERV * CTLIO Y IOIRQ N IOBUS N SSLC CSRW ** - STATE ** - XFER ** - SERV - PSERV - BUF - IOIRQ - IOBUS - LP CMD ** - CSRW ** - XFER ** - SERV - STATE - IOIRQ - IOBUS - MS CMD ** * INCO ** Y CSRW ** * XFER ** Y STATE Y SERV Y CTLIO Y IOIRQ N IOBUS N GIC CMD ** * CSRW ** Y CSIRQ ** Y SERV ** Y XFER ** * BUF Y STATE Y IMBRQ N IMBUS N ADCC CMD ** * CSRW ** N CSIRQ ** Y XFER ** Y SERV Y PSERV N IMBRQ ** N IMBUS N DC CMD ** * INCO ** Y STATE Y SERV Y MA CMD ** * INCO ** * XFER Y STATE * SERV * CTLIO Y LPA CMD ** - INCO ** - XFER ** - BUF - STATE - SERV - Default is no states are stored. - TRACE WINDOW [MODIFY] {ENABLE | =[;...]}[ OR ...] - TRACE WINDOW ENABLE Enables per-device windowing to restrict the states that are stored. Window sources and values are: Device Source Value Qualifier Value ------ ----------- ----------------- ---------- ---------- CPU INSTRUCTION IGNORE IOP DEVNO MCL (none) SEL DEVNO MPX DEVNO CPP CHANNEL ATCD CHANNEL ATCC CHANNEL SCMB (none) CLK (none) IMBA (none) DS UNIT SSLC (none) LP (none) MS UNIT GIC BUS ADCC PORT DC BUS MA UNIT LPA (none) Default is ENABLE (all sources are unrestricted). - TRACE EXECUTE Clears the trace buffer of any prior run and begins trace acquisition using the current settings. If the trigger setting is not ANYTHING, the command prints "Waiting for trigger" on the console. If the trigger is ANYTHING, or once CPU execution is resumed and the trigger is detected, "Trace in process" is printed on the console. Once the required additional trace states are captured, "Trace complete" is printed on the console. - TRACE HALT Stops trace acquisition without waiting for the trigger condition or for the additional trace states to be acquired and prints "Trace halted" on the console. - TRACE [-N] LIST - TRACE [-N] LIST= - TRACE LIST DISABLE Formats the trace buffer and writes the results to the previously specified trace file (if the filename is omitted), or to the specified file. Adding the -N switch causes the trace file to be cleared before writing. If trace memory is empty when the filename is specified, the file is assigned without writing anything. Specifying DISABLE closes the previously written trace file without specifying a new file. Trace formatting is handled by the originating device simulator, except for these storage states: - DATA and FETCH are handled by MCL - IMBRQ and IMBUS are handled by IMB Although handled by the above devices, the originating device name still appears in the trace list. Default is no trace file. - SHOW TRACE Display the current trace specification. --------------------------- Trace Specification Display --------------------------- Maybe: Trace List is empty | Waiting for trigger List on trace.log Memory is 100K states Trigger on instruction is ADD,NOP or address is 00.000023 and data is 000006 Trigger position is start of trace Store on CPU=INSTR;DATA;REG;OPND;IRQ or MCL=DATA or ADCC=CMD;CSIRQ;XFER;IMBRQ Window enabled on CPU=NOP,DELB and don't care is 000002 or ADCC=2-4 Or: Trace Specification LIST on trace.log waiting for trigger MEMORY is 100K states TRIGGER on CPU instruction is ADD,NOP or MCL address is 00.000023 and data is 000006 position is start of trace STORE on CPU=INSTR;DATA;REG;OPND;IRQ or MCL=DATA or ADCC=CMD;CSIRQ;XFER;IMBRQ WINDOW on CPU=NOP,DELB and don't care is 000002 or ADCC=2-4 ---------------- Proof of Concept ---------------- There are currently 468 instances of "tprintf "tpprintf and "hp_trace" calls, 25 of which are used by the CPU (not counting the memory references). It might be a good idea to implement just CPU tracing to start, just to determine the feasibility and scope of the project, as well as the overhead involved. The breakdown of the CPU calls is as follows: - memory read TRACE_DATA, TRACE_FETCH - memory write TRACE_DATA - instruction exec TRACE_INSTR - TOS registers TRACE_REG - base registers TRACE_REG - trap TRACE_INSTR | TRACE_IRQ - interrupt TRACE_INSTR | TRACE_IRQ - simulation stop any - segment resolution TRACE_OPND - STT entry TRACE_OPND - STT resolution TRACE_OPND - clock service TRACE_PSERV - simulation rate TRACE_PSERV - exec separator TRACE_EXEC - LRA modifier TRACE_OPND - dispatch interrupt TRACE_INSTR | TRACE_IRQ - IXIT return TRACE_INSTR | TRACE_IRQ - LST execution TRACE_OPND - SST execution TRACE_OPND - decimal conversions TRACE_OPND - COBOL instructions TRACE_OPND -------------- Implementation -------------- Tracing is initiated by dynamic allocation of an event array and setting the appropriate debug flags in the device. Because tracing is in-memory, the debug log file need not be opened until writing is requested, nor is tracing qualified by the open file. Note that on Windows (at least), allocating and clearing a large array with "calloc" only involves allocating VM space. Memory commitment does not occur until the area is written. So allocating one byte or one million bytes takes the same imperceptible time. The trace array elements are structures as follows: typedef struct { TRACE_ID record_id; DEVICE *dptr; uint32 p1, p2, p3, p4, p5, p6, p7, p8, p9; void *vp; } TENT; Note that setting nine individual fields vs. setting nine array elements generates the same code. Also note that copying structures is implemented by loading and storing each individual field. Each current trace statement in each module must be assigned a record ID to identify how the parameters are interpreted. When the array is formatted and printed, the record ID determines which trace line will be generated. The device pointer points at the device being traced. The integer parameters contain from zero to nine values that depend on the record ID. For example, the CPU register display assigns the registers SBANK, SM, SR, RA, RB, RC, RD, X, and STA to the nine parameters. The void pointer is used for memory traces to store the device pointer and if one or more string values must be stored (if more than one, parameter values are used to indicate the starting index of each string in the concatenated block). Currently, this is used only for the CPU operands of the COBOL-II Instruction Set. These globals are used: FILE *tpf; // trace log file TENT *tps; // trace entry array start pointer TENT *tpe; // trace entry array end pointer TENT *tp; // trane entry current state pointer TENT *tpt; // trace entry trigger state pointer TENT *tpls; // trace entry start state pointer TENT *tple; // trace entry end state pointer t_bool tw; // trace buffer wraparound flag All pointers begin as NULL, and the booleans begin as FALSE. A trace buffer is defined if tps is non-NULL. Tracing is executing if tp is non-NULL. Tracing has triggered if tpt is non-NULL. Tracing is complete when tp equals tple. Commands perform the following actions: TRACE BUFFER= if (tps != NULL) // if a prior buffer exists free (tps); // then free it tp = tpt = NULL; // tracing is inactive and not triggered if (count == 0) // if no buffer is wanted tps = tpe = NULL; // then clear the buffer limits else { // otherwise tps = malloc (count * sizeof (TENT)); // tps points at first element if (tps == NULL) // if out of memory return "Memory exhausted" // then the command fails tpe = tps + (count - 1) * sizeof (TENT); // and tpe points at last element } TRACE EXECUTE if (tps == NULL) // if the trace buffer has not been allocated return "Command not allowed" // then the command fails tp = tps - 1; // tp points at zeroth element (legal?) tw = FALSE; // wraparound has not occurred if (trigger == anything) { // if triggering immediately tpt = tps; // then the trigger has occurred tpls = tps; // and the trace list starts at the buffer start tple = tpe; // and ends at the buffer end cprintf ("Trace in process\n"); } else { // otherwise the trigger is pending tpt = NULL; // so the trigger has not occurred tpls = NULL; // and the trace list has not started tple = NULL; // and the trace list has not ended cprintf ("Trace executing\n"); } TRACE HALT if (tps == NULL) // if the trace buffer has not been allocated return "Command not allowed" // then the command fails if (tw) // if wraparound has occurred if (tp == tpe) // then if pointing at the last entry tpls = tps // then the list starts with the first entry else // otherwise tpls = tp + 1; // the list starts with the next entry else // otherwise with no wraparound tpls = tps; // the list starts with the first entry tpt = tple = tp; // the list ends with the current pointer tp = NULL; // and tracing stops cprintf ("Trace halted\n"); TRACE FORMAT { = } ... The code to trace the CPU registers is: if (tp != NULL // if trace is executing && cpu_dev.dctrl & TRACE_REG) { // and tracing registers TENT *tpx; // then declare a temporary pointer if (tp == tpe) { // if pointing at the last entry tp = tps; // then point at the first entry tw = TRUE; // and set the wraparound flag } else // otherwise tp++; // point at the next entry if (tw // if wraparound has occurred && tp->vp != NULL) // and the block pointer is in use free (tp->vp); // then free its target tpx = tp; // set the temporary if (tp == tple) { // if this is state completes the list tp = NULL; // then stop tracing cprintf ("Trace complete\n"); } tpx->record_id = Trace_CPU_Registers; tpx->dptr = &cpu_dev; tpx->p1 = SBANK; tpx->p2 = SM; tpx->p3 = SR; tpx->p4 = RA; tpx->p5 = RB; tpx->p6 = RC; tpx->p7 = RD; tpx->p8 = X; tpx->p9 = STA; tpx->vp = NULL; } The "tw" flag is needed to determine the number of valid entries when the array is to be formatted. If the flag is false, then the first element is "tps" and the last is "tp". Otherwise, the first is "tp + 1" and the last is tp, with wraparound occurring from "tpe" to "tps". (Actually, this isn't necessary if "calloc" is used to allocate the trace buffer, because if tp < tpe and (tp + 1)->record_id == 0, then wraparound has not occurred.) If the string pointer "pstr" is not NULL, it must be freed to avoid memory leakage when reusing elements after a wraparound. The pointer should then be set to NULL if it is not used to avoid freeing a memory block upon reuse that has already been freed. The "record_id" is an enumeration constant that uniquely identifies the format in which the element is to be displayed and the device and trace flag involved. While this sequence looks as though it begs to be implemented in a function with a variable parameter list, the overhead of such an implementation is considerable. Just pushing all nine registers on the stack prior to the function call and then pulling them from the stack and storing them in the array element doubles the code size. This can be implemented inline with a macro, but a definition would be needed for each unique parameter count (e.g., "trace_1 "trace_2 etc.). Still, it would result in cleaner and easier to understand code. The first half of the code after the conditional could be implemented as a void function, given that the variables used are all global. A CALL or RET instruction takes only two clock cycles if it is a near call, which it is. It would save a decent amount of code, given the large number of trace invocations. Given a function "trace_element (void)" that returns the TENT pointer to the current state, a macro for CPU registers is: #define trace_9(dptr, flg, id, v1, v2, v3, v4, v5, v6, v7, v8, v9, sp) \ if (tp != NULL && dptr->dctrl & flg) { \ TENT *tpx = trace_element (); \ tpx->record_id = id; \ tpx->dptr = dptr; \ tpx->p1 = v1; \ tpx->p2 = v2; \ tpx->p3 = v3; \ tpx->p4 = v4; \ tpx->p5 = v5; \ tpx->p6 = v6; \ tpx->p7 = v7; \ tpx->p8 = v8; \ tpx->p9 = v9; \ tpx->vp = vp; \ } \ else \ (void) 0 The invocation is: trace_9 (&cpu TRACE_REG, Trace_CPU_Registers, SBANK, SM, SR, RA, RB, RC, RD, X, STA, NULL); Once a trace is completed and a formatting requested, a routine is invoked to scan through the trace array and format and write each entry to the debug log file. Elements are printed by calling "hp_trace" with the device pointer, trace flag, and format string determined by the element's "record_id". It should be possible to use a single call to handle a variable number of formatted parameters, as extra parameters are not consulted if not referenced in the format string. That is, all nine numeric parameters could be specified in the "fprintf" call because only those parameters referenced in the format string will be used. The trigger is driven by the memory address and data specified and so is tested only in the memory read and write routines. The code to trace a memory write is: if (tp != NULL // if trace is executing && dptr != NULL // and tracing is wanted && dptr->dctrl & trace_flag) { // and the appropriate trace flag is set TENT *tpx; // then declare a temporary pointer if (tp == tpe) { // if pointing at the last entry tp = tps; // then point at the first entry tw = TRUE; // and set the wraparound flag } else // otherwise tp++; // point at the next entry if (tw // if wraparound has occurred && tp->vp != NULL) // and the block pointer is in use free (tp->vp); // then free its target tpx = tp; // set the temporary if (tp == tple) { // if this is state completes the list tp = NULL; // then stop tracing cprintf ("Trace complete\n"); } if (address == trigger_address // if the trigger address matches && tpt == NULL // and the trigger is still pending && (value == trigger_data // and the trigger data matches || trigger_data == NOT_USED) { // or the data term is not used tpt = tp; // then set the trigger point if (trigger_position == Start) { // if the trigger position is the start of the trace tpls = tp; // then the list starts at the current entry if (tp == tps) // if the current position is the buffer start tple = tpe; // then the list ends at the buffer end else // otherwise tple = tp - 1; // the list ends at the prior entry after wraparound cprintf ("Trace in process\n"); } else if (trigger_position == Center) { // otherwise if the position is the center halfway = count * sizeof (TENT) / 2; // then get the halfway point if (tp > halfway) // if the current position is more than halfway tpls = tp - halfway; // then the list starts halfway back else // otherwise tpls = tp + halfway; // the list starts halfway ahead if (tpls == tps) // if the list starts at the buffer start tple = tpe; // then the list ends at the buffer end else // otherwise tple = tpls - 1; // the list ends at the prior entry after wraparound cprintf ("Trace in process\n"); } else { // otherwise the trigger position is the end of the trace tple = tp; // so the list ends with the current position if (tple == tpe) // if the list ends at the buffer end tpls = tps; // then the list starts at the buffer start else // otherwise tpls = tple + 1; // the list starts at the next entry after wraparound tp = NULL; // all states are already in the buffer cprintf ("Trace complete\n"); } } tpx->record_id = Trace_Memory_Write; tpx->dptr = dptr; tpx->p1 = classification; tpx->p2 = bank; tpx->p3 = offset; tpx->p4 = value; } ...where "trace_flag" is "mem_access [classification].trace_flag". Tracing memory reads is similar. The "trigger_address" will be the physical address of the trigger, or -1 if the address is not part of the trigger. The "trigger_data" is similarly -1 if not used. ----------------------- The Indirection Problem ----------------------- sio_trace needed by: - mpx - sel mem_trace (MCL) needed by: - cpp - cpu - imba - iop - sel iop_trace (IOP) needed by: - atc - clk - cpu - ds - imba - lp - mpx - ms - sel - sslc imb_trace needed by: - adcc - cpp - cpu - gic - imba - (mem?) Trace chains: - cpu : iop_trace, imb_trace, mem_trace - iop : mem_trace - mpx : iop_trace, sio_trace - sel : iop_trace, sio_trace, mem_trace - scmb : - - atc : iop_trace - clk : iop_trace - lp : iop_trace - ds : iop_trace, dl_trace - ms : iop_trace, tl_trace - sslc : iop_trace - imba : iop_trace, imb_trace, mem_trace - cpp : imb_trace, mem_trace - mem : - - gic : imb_trace - dc : - - ma : tl_trace - lpa : - - adcc : imb_trace ----------------- The Clone Problem ----------------- A problem arises if a device clone is configured with TRACE STORE, state entries in the trace list are written by the clone, then the clone is deallocated, and then a TRACE LIST command is executed. In that case, the device pointers stored in the state entries will be invalid. One way to handle this is to require device adjust routines to call a trace sanitizer routine when clones are being deallocated. If a trace list exists, the sanitizer will search through the entries for matches to the supplied list of clone device pointers and, if found, rewrite the device pointers to reference the original device and add an indication to the state bitmap that the change has occurred. The TRACE LIST command would then see only valid device pointers and would mark the entries in the list that correspond to deleted clones (as the listed device names would be those of the base devices rather than the clone names). This scheme has the advantage that the trace list need be scanned only when it exists and clones are being deleted. It would, however, perform the scans even when the deleted clones are not part of the STORE specification. It also has the disadvantage that the name shown is not the name of the cloned device, although that may also be true of the original device if it lost its clone renaming. Also, if a device adjust routine skips the sanitizer call, the TRACE LIST command will encounter invalid pointers, which might crash the simulator. An alternate scheme would be to validate each device pointer in the TRACE LIST command. As this would be done for every list request, validation must be fast. One implementation would be for the TRACE STATE command to build a clone mapping table. This static table would be "SIM_DEVICE_SIZE" elements of a structure containing the clone device pointer, the base device pointer, and the index of the clone in the "sim_devices" array. This table would built during the existing full device scan to determine the longest device and state name in the configuration, with a NULL entry added to terminate the list. For each device that has storage states set, the DEV_CLONE flag would be checked. If the flag is clear, then no entry is made in the mapping table. If it is set, the clone device pointer and index are entered into the table. Then the device table is scanned backward until a device without the DEV_CLONE flag is found. This would be the base device, and its device pointer is also entered into the mapping table, along with the relative index between them (because an absolute index will change if an intervening clone is deleted). In this way, the table would contain entries for all clones that are configured to write trace records to the list. In practice, this list will be empty or very small. In the TRACE LIST command, the mapping table would be searched for each entry's device pointer. If it was not found, then formatting may be performed. If it is found, then the clone device pointer is validated by (a) verifying that the "sim_devices" entry at the relative mapping index is the same, and (b) that the clone device's "adjust" pointer is the same as the base device's "adjust" pointer. If validation succeeds, formatting continues with the clone device. If it fails, the base device is substituted along with an indicator that the substitution was made. This scheme has the advantage that it is fully automatic and would eliminate the possibility of using invalid pointers if a device "adjust" routine fails to call the sanitizer. It has the disadvantage that the mapping table search adds overhead to every state entry formatting, although the overhead should be small, given that the mapping table will contain at most a few entries. It still has the disadvantage that the original device name (plus indicator) is shown instead of the cloned device name. An improvement on this would be to build a second table from the first during the LIST command prelude. The second table would contain only those clone entries that have been deleted, as determined by the validation process described above. That way, if the clones still exist, then the second table will be empty. Then the per-state-entry check need not perform validation but only a search and replacement for a deleted clone. A better improvement might be to add a copy of the clone name to the table built during the TRACE STORE command. For each entry, a memory block would be allocated to hold a copy of the clone device name, and a pointer to the block would be saved in the entry. The initial scrub of the table prior to reuse would need to deallocate any name blocks found. In the TRACE LIST prelude, the second table would be built as above. In the list loop, however, the search and replacement need not occur. Instead, the invalid clone pointer is passed to the device's trace handler, which then passes it back to the "trace_format" routine. In that routine, the search is performed, and the substitution of the original device for the cloned device is made, except that the clone device name is formatted instead. The state name lookup is still performed, but using the original device. This has the advantage of using the correct clone name in the list. The original device renaming is lost if all clones are deallocated, but even though the original name is used, it is still unique and therefore correctly identifies the device that recorded the state. This scheme does depend on the trace handlers not to dereference the entry device pointer, as it may be invalid. However, none do, except for the CPP handler, and that module cannot be cloned. ------------------------------------ Tracing the COBOL "EDIT" Instruction ------------------------------------ The EDIT instruction presents a problem for tracing, as the instruction consists of one or more sub-opcodes and parameters following the EDIT opcode. Consequently, the length of the data that must be stored in the trace entry is not known until the sub-opcode list is examined. The current trace regimen calls "fprint_edit" to print the current sub-opcode and its parameter(s). This routine is also called from the "fprint_subop" routine that is used to print EDIT sub-operations for the EXAMINE command when used with the -E switch. "fprint_edit" takes an output stream, radix, and starting byte address as parameters. It formats and prints one EDIT sub-operation and returns the number of bytes consumed. For memory traces, two approaches are possible. First, the trace entry "vptr" parameter could point to a string formatted from the sub-operation. Second, "vptr" could point at a byte array containing the bytes that form the sub-op and its parameters, with formatting deferred until the trace log is written. The first approach requires that "fprint_edit" produce a formatted string rather than formatted output. More precisely, "fprint_edit" would call a new "fmt_edit" routine that would format the string, while the former would then simply write the string to the output stream. The memory trace would call "fmt_edit copy the result to an allocated block, and store the pointer in the trace entry. This approach has the advantage that the bytes forming the sub-operation in memory are used directly, obviating the copy that would otherwise be necessary to use them later. It has the disadvantage that the formatting is done at execution time rather than at trace formatting time. This is wasted effort if the trace entry is overwritten during a wraparound. It also requires a new routine that implements the variable-radix output of "fprint_val" but with the result written to a string. The second approach eliminates these disadvantages. However, it would require that "fprint_edit" be rewritten to obtain all bytes from a buffer, rather than directly from memory, and an EXAMINE command would have to copy the bytes from memory before passing them to the "fprint_edit" routine. This would not be a burden, though, as it would occur at command-input time and so would likely go unnoticed. Also, the first three or four bytes are already present in the "sim_eval" array, and all but three sub-operations use no more than three bytes. The implementation is therefore: - void mem_copy_bytes (uint32 byte_address, uint32 byte_count, uint8 *target) Copies "byte_count" bytes from memory starting at "byte_address" into the byte array pointed at by "target". - uint32 cpu_cis_edit_count (uint8 opcode, uint8 operand) Examines the opcode and operand and returns the number of bytes occupied by the sub-op and its parameters. - uint8 *trace_edit (uint32 byte_address, uint32 byte_count) Allocates a byte array of the indicated size, and then calls "mem_copy_bytes" to copy the sub-op and parameters from memory into the array, returning a pointer to the array. The byte count is determined by calling "cpu_cis_edit_count" and passing the current opcode and operand. - void fmt_edit (FILE *stream, uint32 radix, uint8 *buffer) Decodes the EDIT sub-operation and parameters from the buffer, formats the operation using the specified radix, and writes the result to the output stream. - uint32 fprint_edit (FILE *stream, t_value *val, uint32 radix, uint32 byte_address) Copies the first three bytes from the "val" array into a 260-byte buffer, then calls "cpu_cis_edit_count passing the first two bytes as the opcode and operand, to determine the extent of the sub-operation. If the extent is larger than three bytes, it calls "mem_copy_bytes" to copy the remaining parameters from memory into the buffer. Finally, it calls "fmt_edit passing a pointer to the buffer, to format and write the sub-operation to the output stream. - t_stat cpu_cis_trace (TRACE_ENTRY *eptr, STATE_LOCATION location) For EDIT sub-ops, calls "fmt_edit" to format and print the sub-op and parameters contained in the buffer that is addressed by "vptr". -------------------- CPU Tracing Overhead -------------------- The worst overhead comes from tracing CPU instructions. This is because the trace calls are made for every instruction execution. The CPU offers four storage states that occur with each instruction: - FETCH (memory instruction fetches) - INSTR (instruction executions) - DATA (memory data accesses) - REG (register values) Data states do not occur on every instruction but sometimes occur more than once per instruction. So they probably average out to once per. Also, register states occasionally include the set of segment registers, although this is infrequent compared to the others. Timing tests with the SQR program show these CPU speeds for various loads: Speed States ----- --------------------------------- 12x (none) 9x FETCH 9x INSTR 9x DATA 9x REG 8x INSTR, DATA 8x INSTR, REG 7x INSTR, DATA, REG 6x FETCH, INSTR, DATA, REG 11x OPND 11x IRQ 9x INSTR, IRQ 7x INSTR, DATA, REG, IRQ 7x INSTR, DATA, REG, OPND 7x INSTR, DATA, REG, OPND, IRQ ------------- Counting Time ------------- To count time intervals, a "time" field is added to the TRACE_RECORD structure. The time value could be derived either from the SCP global time via the "sim_gtime" routine or from a new 64-bit integer time kept in the trace module. SCP global time is kept in three variables: - static double sim_time - int32 sim_interval - UNIT *sim_clock_queue->(int32) time Presumably, time is kept as an integer value in a double-precision floating-point variable to provide 53 bits at a time when 64-bit integer support was not available. Historically, though, floating-point operations were expensive, so during execution, "sim_time" is not updated unless a "sim_process_event", "sim_activate", or "sim_cancel" call is made. Instead, "sim_interval" is initially set to the "time" field of the unit at the head of the event list via "sim_clock_queue->time" and then is decremented to count time. The "sim_gtime" routine calculates the current time as: sim_time = sim_time + (sim_clock_queue->time - sim_interval); sim_clock_queue->time = sim_interval; That is, the amount of time elapsed from the interval is added to the last "sim_time" update to obtain the current time, and then the interval is reset to the amount of time remaining before the next event. In addition, time is separately kept in an unsigned 32-bit integer variable "sim_rtime" that was added at version 2.6. However, this time may roll over during execution. For the HP 1000 simulator, it will roll over approximately every 45 minutes of "real" time; the "sim_time" variable will roll over approximately every 181 years. The simplest implementation would be to save the time in a trace record field as a double-precision value and obtain it by calling the "sim_gtime" routine. While floating-point instructions are typically as fast as integer instructions today, the problem is that the "sim_gtime" routine essentially does this: if (sim_clock_queue == NULL) { sim_time = sim_time + (noqueue_time - sim_interval); sim_rtime = sim_rtime + ((uint32) (noqueue_time - sim_interval)); noqueue_time = sim_interval } else { sim_time = sim_time + (sim_clock_queue->time - sim_interval); sim_rtime = sim_rtime + ((uint32) (sim_clock_queue->time - sim_interval)); sim_clock_queue->time = sim_interval } return sim_time; ...which yields a 20-instruction path for the non-empty queue case (which is always true with the HP simulators). Note that there's no obvious reason why "sim_gtime" has to update the time; calculating it should be sufficient, as the aforementioned set of event support routines performs the updates as needed. If "sim_time" were made global, then this could be reduced by replacing "sim_gtime" with an extension module routine that just returned the calculated time, e.g.: double ex_sim_gtime (void) { return sim_time + (double) (sim_clock_queue->time - sim_interval); } That yields: subl $12, %esp movl _sim_clock_queue, %eax movl 24(%eax), %eax subl _sim_interval, %eax movl %eax, 4(%esp) fildl 4(%esp) faddl _sim_time addl $12, %esp ret The floating-point instructions are both single-cycle, and the return value is in floating-point register "st0". Storing the value in a double-precision field in the trace record is accomplished by a single FSTP instruction. Note, though, that the 32-bit difference must be stored in a temporary stack location before it can be converted and loaded into a floating-point register, as the FP unit cannot exchange directly with the integer registers. Another possibility is to fix the "sim_gtime" output and store the time as a 64-bit integer in the trace record. This generates a single FISTTP instruction, which also executes in a single cycle, although it imposes a 6-cycle latency (which isn't significant in this case). If "sim_gtime" is replaced with a routine that returns an integer, then this: t_int64 ex_sim_qtime (void) { return (t_int64) sim_time + (sim_clock_queue->time - sim_interval); } ...could be used to obtain the current time for the trace record. It generates: pushl %ebx subl $8, %esp movl _sim_clock_queue, %eax fldl _sim_time fisttpq (%esp) movl 24(%eax), %eax subl _sim_interval, %eax cltd addl (%esp), %eax adcl 4(%esp), %edx addl $8, %esp popl %ebx ret It's not clear why register "ebx" is saved and restored. If the calculation is made inline via a macro, then storing the result in the trace record generates: fldl _sim_time fisttpq 24(%esp) movl _sim_clock_queue, %eax movl 24(%eax), %eax subl _sim_interval, %eax cltd addl 24(%esp), %eax adcl 28(%esp), %edx movl %eax, 16(%ecx) movl %edx, 20(%ecx) An alternate implemetation is to keep our own "trace clock" as a 64-bit integer. To do this requires two things. First, every decrement or increment of "sim_interval" must be paired with an increment or decrement of the trace clock. This can be accomplished by defining and using an EVENT_TICK macro that performs both actions. The trace-clock portion generates these instructions: addl $1, _trace_clock adcl $0, _trace_clock+4 Second, the "sim_idle" routine must be extended by a "trace_sim_idle" routine that saves the "sim_interval" value, then calls "sim_idle", and then gets the difference between the new value and the saved value. This represents the time during which the simulator is suspended, which may be zero if idling is inhibited or the time is less than the minimum suspension time. This difference is then added to the trace clock. Saving the current time in the trace record generates these instructions: movl _trace_clock, %edx movl _trace_clock+4, %ecx movl %edx, 16(%eax) movl %ecx, 20(%eax) This implementation has the advantages of reducing the tracing overhead, which is especially significant for the CPU instruction traces, and it requires no changes to the SCP, although it does require a "sim_idle" extension. It also complicates the "sim_interval" handling, requiring the substitution of an EVENT_TICK macro call for a simple "sim_interval" increment or decrement, which distributes a small amount of extra overhead to the instruction loop, even when tracing is inactive. --------------------------- The Calibrated Time Problem --------------------------- When counting time, the trace list shows the relative or absolute time between states. These times are "real times" calculated from the event tick counts pertaining to the captured states. A problem occurs, however, when capturing states that are derived from the system keyboard poll timer, e.g., the SSLC receiver poll time. This timer is calibrated, meaning that it adjusts itself to track the wall-clock time. the result of this is that the nominal 10 millisecond poll period is rendered as some absurdly large value, e.g., 2.5 seconds, due to the mismatch between real time and calibrated time. Note that, in real-time terms, the states really are 2.5 seconds apart, due to the very long delay needed to achieve wall-clock tracking. The problem is that the value is meaningless to the user of the trace list, who is expecting polls to be shown as occurring every 10 milliseconds (which they would if the keyboard poll timer were uncalibrated). For relative times, the problem could be fixed by inserting a non-displaying corrective record when the keyboard timer event occurs. This record's "time" field would be set to the resumption time less a count equivalent to 10 milliseconds of real time. This would ensure that a state record immediately following the pause would see a relative time of 10 milliseconds. It's important to note that there may be no state record between keyboard events. Rather than accumulate a bunch of correction records, a single record could be updated to show 20 milliseconds, 30 milliseconds, etc., only adding a new correction record if the last record is not a correction record. Because all of the calibrated-time events are co-scheduled with the keyboard poll timer, and because they are all queued behind that event service, the correction can be performed by the keyboard event service routine, as follows: if Trace_Active in trace.modes then trace_correct_time; end if; The trace_correct_time routine is: if trace_state /= Trace_Empty then current_cal_time := sim_gtime; last_cal_time := trace.current->time; delta_cal_time := current_cal_time - last_cal_time; if delta_cal_time < uptr->wait then delta_real_time := POLL_PERIOD * delta_cal_time / uptr->wait; else delta_real_time := POLL_PERIOD end if; if delta_real_time > 0 then if trace.current->dptr = NULL then entry := trace.current; entry->p [0] := entry->p [0] + delta_real_time; entry->p [1] := entry->p [1] + delta_cal_time - delta_real_time; else entry := trace_enter; entry->dptr := NULL; entry->record_id := 0; entry->p [0] := delta_real_time; entry->p [1] := delta_cal_time - delta_real_time; end if; entry->time := current_cal_time - entry->p [0]; end if; end if; The operation is as follows: 1. If no states have been stored yet, then no correction is needed. 2. If a state exists, then the difference between the current time and the last state time is obtained. 3. If the last state is a device state, then the time difference must be less than or equal to the poll time (because it must have been logged after the last poll event). In this case, the calibrated difference is converted to the real difference by multiplying the real poll time by the fraction of the calibrated poll time represented by the time difference. 4. If the last state is a correction state, then the time difference may be greater than the poll time due to the subtracted correction. In this case, the real difference is the real poll time (because no other states have been recorded since at least the last poll event). 5. The relative time correction is the real time difference, while the absolute time correction is the sum of the calibrated differences less the real differences between the current record and the trigger record. Relative time correction is applied by moving the current calibrated time back by the real difference, which makes the current and next record appear to be separated by the real time difference. Absolute time correction is applied by moving the time of each device state record before or after the trigger record forward or backward by the current sum of the difference between the calibrated difference and the real difference, which makes all of the calibrated time gaps appear to be real time gaps. 6. If the real difference is zero, then no correction is needed. This will occur when the poll time is initialized and before it has been calibrated. 7. If the correction is needed, then if the last state record is a correction record, it is reused; the cumulative real difference is kept in parameter 0, and the cumulative absolute difference is kept in parameter 1. If the last state is a device record, then a new record is allocated and set up as a correction record, with parameter 0 set to the (initial) real difference and parameter 1 set to the absolute difference. 8. Finally, the correction record's calibrated time is set to the current time less the cumulative real correction (the correction record's time is not used for absolute time displays). For a new correction record, the time will be some fraction of the real poll time less than the current time, so an immediately following device record will appear to be that fractional poll time later in relative time (e.g., +10.000 msec). For a prior correction record, the time will be some multiple of the poll time earlier (e.g., +20.000 msec). To count absolute times, the list routine must apply the corrections to the times preceding and following the trigger time. First, the routine sums all of the corrections from parameter 1 of each correction record preceding the trigger. Then for each device state record preceding the trigger, the absolute time would be the trigger time less the record time less the cumulative correction. For each correction record encountered, the cumulative correction would be decreased by the correction in the record until that correction reaches zero after the last correction before the trigger. At the trigger state, the cumulative correction would be zero. From then on to the end of the list, the corrections would accumulate, with the absolute time being the record time less the trigger time less the cumulative correction. ----------------------- The PAUS Timing Problem ----------------------- When counting time, the trace list shows the relative or absolute time between states. These times are "real times" calculated from the event tick counts pertaining to the captured states. A problem occurs, however, when capturing states that are derived from the system keyboard poll timer, e.g., the SSLC receiver poll time. This timer is calibrated, meaning that it adjusts itself to track the wall-clock time. the result of this is that the nominal 10 millisecond poll period is rendered as some absurdly large value, e.g., 980 seconds, due to the mismatch between real time and calibrated time. Note that, in real-time terms, the states really are 980 seconds apart, due to the very long delay needed to achieve wall-clock tracking. The problem is that the value is meaningless to the user of the trace list, who is expecting polls to be shown as occurring every 10 milliseconds (which they would if the keyboard poll timer were uncalibrated). The problem is exacerbated when the operating system executes a PAUS instruction, and the simulator is idled. The trace clock is not updated by the "sim_idle" routine to account for the idle sleep, and so captured poll states appear to be occurring every 2.57 microseconds when the system is idle. For relative times, the problem could be fixed by inserting a non-displaying corrective record when the pause occurs. This record's "time" field would be set to the resumption time less a count equivalent to 10 milliseconds of real time. This would ensure that a state record immediately following the pause would see a relative time of 10 milliseconds. It's important to note that there may be no state record between pauses. Rather than accumulate a bunch of correction records, a single record could be updated to show 20 milliseconds, etc., only adding a new correction record if the last record is not a correction record. It's also important to note that a PAUS instruction execution may not presage an idle period. It's possible that a disc transfer, e.g., is ongoing, in which case the event queue will have DS device events repeatedly scheduled. If this occurs for the entire time prior to the interrupt that resumes from the pause, then no correction is needed. A more common case would be that the poll timer is present in the list but behind one or more other events. In this case, the correction is needed only when the poll event arrives at the head of the queue. This can be identified by the UNIT_IDLE flag in the UNIT structure. When this occurs, no other states can intervene, and the correction is needed for the remainder of the 10 millisecond period. During a pause, the sim_idle routine may or may not be called. It depends on idling being enabled and no IMB or IOP channel requests being active. If it is called, it may or may not suspend execution, depending on whether more than one millisecond of calibrated time remains before the next event. The routine will return TRUE if a suspension occurred and FALSE if it did not. If suspension occurs, sim_interval will be decreased by an amount corresponding to the time suspended. If it does suspend, it may or may not suspend for the entire remaining time, depending on the number of integral calibrated milliseconds remaining. The suspension time may be determined by the difference between the sim_interval values before and after the call. The trace_clock must be updated by this value because only sim_interval is updated by the sim_idle call. Corrections can be implemented as a new class of hidden internal records that have NULL device pointers and record IDs that identify the specific type of internal record. For pause corrections, an ID of zero is used. An initial implementation sets a pause correction flag in the PAUS instruction executor: cpu_micro_state = Paused; if (trace_modes & Trace_Active) trace_modes |= Trace_Pausing; Then in the instruction loop: // cpu_micro_state == Paused else if (sim_clock_queue->flags & UNIT_IDLE // idle-able && not (SIR & sir_IMBCSRQ) // no IMB chan req && not sel_request // no sel chan req && mpx_request_set == 0) { // no mpx chan req if (trace_modes & Trace_Pausing) trace_pause (TMR_POLL); if (sim_idle_enab) { last_interval = sim_interval; if (sim_idle (TMR_POLL, FALSE) == TRUE) // if it suspended trace_update (last_interval); // is-> trace_clock += (last_interval - sim_interval); } } The trace_pause routine in the trace module is: void trace_pause (uint32 timer) { TRACE_RECORD *entry; if (not (trace_modes & Trace_Active)) // no longer active? return; if (trace.current->dptr == NULL) // current is correction? entry = trace.current; // reuse else { entry = trace_enter (); // new entry entry->dptr = NULL; entry->record_id = 0; entry->p [0] = 0; entry->p [1] = 0; } // compute real time equivalent to calibrated time remaining delta = POLL_PERIOD * sim_interval / hp_sync_poll (Service); // get amount of absolute correction entry->p [0] += sim_interval - delta; // move the clock ahead by the interval less the real time correction entry->time = trace_clock + entry->p [0]; trace_modes &= ~Trace_Pausing; return; } Detection of a prior correction record depends on execute_cmd setting the initial trace.current (i.e., trace.limit) entry's device pointer non-null when the list is empty. ----------------------------- The EX_TRACE_HANDLER Function ----------------------------- Per-device trace operations are implemented by defining a trace handler function that is accessed via a pointer in the device extension structure. The function takes these parameters: EX_TRACE_COMMAND command -- the trace command uint32 instance_id -- the identifier of the device instance FILE *stream -- the trace list output stream pointer void *xptr -- a pointer to the string to parse or state record to list uint32 radix -- the radix override from the command line The trace command is one of these enumeration identifiers: Trace_Set_Trigger -- parse and set the trigger conditions Trace_Show_Trigger -- parse and set the window conditions Trace_Set_Window -- parse and set the state storage conditions Trace_Show_Window -- display the current trigger conditions Trace_Set_Store -- display the current window conditions Trace_Execute -- prepare for trace execution Trace_Set_List -- prepare for trace listing Trace_Show_List -- format and write the acquired trace states Command actions are indicated by the following parameter associations: command stream xptr Action ------------------- ------ ------ ----------------------------------------- Trace_Set_Trigger NULL NULL Clear trigger Trace_Set_Trigger NULL string Parse and set trigger from string Trace_Show_Trigger file string Print trigger on file using string prefix Trace_Set_Window NULL NULL Clear window Trace_Set_Window NULL string Parse and set window from string Trace_Show_Window file string Print window on file using string prefix Trace_Set_Store NULL NULL Clear storage states Trace_Set_Store NULL string Parse and set storage states from string Trace_Execute NULL scalar Initialize trace execution Trace_Set_List NULL NULL Initialize trace listing Trace_Show_List file entry List trace entry on file Command-line radix override switches are valid for the TRACE TRIGGER, TRACE WINDOW, and SHOW TRACE commands. The supplied "radix" parameter is always zero for the TRACE STORE, TRACE EXECUTE, and TRACE LIST commands. The handler returns a status value that indicates the success or failure of the action, as follows: Command Status Meaning ------------------ ------------ ------------------------------------------ Trace_Set_Trigger SCPE_OK Trigger conditions were parsed and set SCPE_* Trigger condition parsing failed SCPE_NOFNC Triggering is not supported by this device Trace_Set_Window SCPE_OK Window conditions were parsed and set SCPE_* Window condition parsing failed SCPE_NOFNC Windowing is not supported by this device Trace_Set_Store SCPE_OK Storage conditions were parsed and set SCPE_* Storage condition parsing failed Trace_Show_Trigger SCPE_OK Trigger conditions were displayed SCPE_INCOMP No trigger conditions were displayed Trace_Show_Window SCPE_OK Window conditions were displayed SCPE_INCOMP No window conditions were displayed Trace_Execute SCPE_OK Trace execution may proceed SCPE_* Trace execution cannot proceed Trace_Set_List SCPE_OK List reporting was initialized SCPE_* List reporting failed Trace_Show_List SCPE_OK The trace record was formatted and written SCPE_IERR The trace record ID was invalid ...where SCPE_* is any status code other than the ones listed. In addition to the status code, the TRACE EXECUTE command must clear the participation Boolean via the supplied pointer if the device is not currently involved in the trace specification. A module is involved if it is configured to trigger or store states and will be assumed to be involved unless the participation variable is cleared. Participation is required to implement the secondary participation table that is used to recover a device identifier when a secondary device instance has been deleted between trace acquisition and listing. The function is implemented by an eight-way switch statement that typically executes each command in-line. However, some commands are complex and are implemented by subroutines. Also, the Trace_Set_Store action is virtually always implemented by calling the "ex_trace_parse_states" routine and passing the device pointer and the TRACE_IMPLICIT set of flags (there is no equivalent Trace_Show_Store, as this is already handled in the SCP trace module). An alternate to this implementation is to define separate functions for each of the actions (or perhaps for each pair of actions) and to have the device extension structure field point at a structure containing pointers to these separate functions (and the TRACE_IMPLICIT value, which would allow the Trace_Set_Store command to be handled within the SCP trace module). Commands that currently return SCPE_NOFNC or SCPE_INCOMP because they do not support the associated actions would place NULL pointers in the structure. Separate functions would allow command-specific parameters, as follows: typedef t_stat EX_TRACE_TRIGGER_HANDLER (uint32 instance_id, FILE *stream, char *cptr, uint32 radix); typedef t_stat EX_TRACE_WINDOW_HANDLER (uint32 instance_id, FILE *stream, char *cptr, uint32 radix); typedef t_stat EX_TRACE_LIST_HANDLER (uint32 instance_id, FILE *stream, EX_TRACE_RECORD *eptr); typedef const struct { EX_TRACE_TRIGGER_HANDLER *trigger_handler; EX_TRACE_WINDOW_HANDLER *window_handler; EX_TRACE_LIST_HANDLER *list_handler; uint32 implicit_set; } EX_TRACE_HANDLER; For the SET/SHOW TRIGGER and SET/SHOW WINDOW commands, the "stream" parameter would indicate whether the action is SET (non-NULL) or SHOW (NULL). The SET and SHOW STORE actions would be handled within the SCP trace module using the "implicit_set" of trace states. For LIST command, "stream" is the output stream, and the "eptr" parameter indicates whether this is list initialization (NULL) or list processing (non-NULL). The LIST handler also serves as the execution handler when "stream" is NULL. It returns SCPE_OK to proceed with participation, SCPE_INCOMP to proceed without participation, and another SCPE error to abandon execution. Every device that supports tracing needs both an execute handler and a list handler, because every device at least supports the STORE action, so combining the two is efficient. The trigger and window handlers use the instance ID to set or show the configurations from the proper instance. The list handler uses the ID only when doing execution initialization (to check the appropriate device state flags field) and potentially for list initialization. It is not needed for list processing. One downside is that complex trigger or window actions use separate parser routines. So they would need, e.g., a trigger handler that calls a trigger parser and a window handler that calls a window parser, potentially increasing the number of routines required. But perhaps these can be combined into single routines. The current flow is: trigger_cmd -> parse_device (Trace_Set_Trigger) -> trace_device (Trace_Set_Trigger) -> ex_trace_parse_sources (trigger_table) -> [loop] parse_trigger (cmd_id, cptr) "trigger_cmd" calls "parse_device, which calls "trace_device" with the portion of the command line after removing the device name. "trace_device" initializes a new trigger structure, then calls "ex_trace_parse_sources" and the configures the trigger upon a successful return. "ex_trace_parse_sources" calls "parse_trigger" in a loop, once for each trigger source specified in the command. A similar flow is used for the "window_cmd" processing. An alternative might be for "trigger_cmd" to call a "trigger_device" routine that performs all of the processing. It would intialize the new trigger structure, execute a loop that calls an "ex_trace_parse" routine that takes a parsing table as a parameter and returns a command ID and a pointer to the operand value. The ID would be used to dispatch to the appropriate handler that currently resides in the parse_trigger routine. Once the line is exhausted, it would configure the trigger source. The return structure would be: typedef struct { /* the parse return structure */ int32 source_id; /* the source identifier */ char *operand; /* a pointer to the operand */ } EX_TRACE_PARSE_SOURCE; A "trigger_device" routine would look like this: t_stat trigger_device (uint32 instance_id, FILE *stream, char *cptr, uint32 radix) { EX_TRACE_PARSE_SOURCE source; TRACING_MODES new_modes; /* the new trigger operational modes */ TRIGGER_STATE new_trigger; /* the new trigger sources */ TRACE_SOURCE last_source; /* the last configuration source specified */ uint32 last_words; /* the last count of instruction words */ t_stat status = SCPE_OK; if (stream != NULL) { /* if showing the trigger set */ [ do trigger show ] } else if (cptr == NULL) /* otherwise if sources are not present */ mode_spec = default_mode; /* then reset to the default mode */ else { /* otherwise */ new_modes = mode_spec; /* set up */ new_trigger = trigger; /* the new trigger set */ last_source = Nothing; /* with the */ last_words = 0; /* existing configuration */ do { /* otherwise */ source = ex_trace_parse_source (&cptr, trigger_table); // returns parse.source_id, parse.operand if (source.source_id < 0) /* if a parsing error occurred */ status = SCPE_ARG; /* then report a bad argument */ else switch (source.source_id) { /* otherwise dispatch on the trigger source */ case Instruction: /* trigger on instruction execution */ ... case Ignore: ... } } while (cptr != NULL && status == SCPE_OK); /* continue until exhaustion or failure */ if (status == SCPE_OK) { /* if parsing succeeded */ mode_spec = new_modes; /* then set the new trigger mode */ trigger = new_trigger; /* and trigger specification */ } } return status; } The new extension parsing routine would look like: EX_TRACE_PARSE_SOURCE ex_trace_parse_source (char **cptr, EX_TRACE_PARSER_TABLE *ptab) { size_t length; char *nptr, gbuf [CBUFSIZE]; EX_TRACE_PARSER_TABLE *entry; EX_TRACE_PARSE_SOURCE source = { -1, NULL }; /* preset for an error return */ if (*cptr == NULL || **cptr == '\0') /* if the conditions are missing */ return source; /* then return failure */ else { /* otherwise */ nptr = strpbrk (cptr, ";\"'"); /* and search for a semicolon or single or double quote */ if (nptr != NULL && (*nptr == '\'' || *nptr == '"')) { /* if a quoted string is present */ quote = *nptr++; /* then save the opening quotation mark */ while (nptr [0] != '\0' && nptr [0] != quote) { /* while characters remain within the quotes */ if (nptr [0] == '\\' && nptr [1] == quote) /* if an escaped quote sequence follows */ nptr = nptr + 2; /* then skip over the pair */ else /* otherwise */ nptr = nptr + 1; /* skip the non-quote character */ } nptr = strchr (nptr, ';'); /* find the end of the current condition */ } if (nptr != NULL) /* if a semicolon was found */ *nptr = '\0'; /* then separate the this condition from the next one */ *cptr = get_glyph (*cptr, gbuf, '='); /* parse the source keyword and point at the value */ length = strlen (gbuf); /* get the length of the supplied keyword */ for (entry = ptab; entry->name != NULL; entry++) /* search the parser table */ if (strncmp (gbuf, entry->name, length) == 0) { /* if a source keyword matches */ source.source_id = entry->id; /* then save the souce identifier */ source.operand = cptr; /* and a pointer to the operand */ break; /* and terminate the search */ } if (nptr != NULL) /* if a semicolon was found */ *nptr++ = ';'; /* then restore it in case of errors */ } return parser; /* return the result of the parse */ } =============== SCP Alterations =============== Bob, I'm working my way toward another HP 3000 release in a couple of months. Along the way, I've uncovered what I believe are anomalous behaviors in SCP. First, when the "get_yn" routine is called, it echoes the supplied prompt to the console log if it's enabled, but it does not echo the user's response. This leaves the log incomplete, with the next command running onto the line containing the prompt. I've added an "fputs" to write the user's response to the log. Also, I note that the default parameter and the function return type are "t_stat". However, the routine returns the values TRUE or FALSE, so I think these should be "t_bool" types instead. I've modified these as well. Second, I'd like to suggest that the SHOW command be altered to suppress the "all units disabled" message when a device has only permanently disabled units. Current behavior for a device having a combination of enabled, user-disabled, and permanently disabled units is to display the sum of the first two types, excluding any permanently disabled units, as the unit count. This is reasonable, as the user cannot alter the state of the permanently disabled units. If the user subsequently disables all of the user-alterable units, then the "all units disabled" message is displayed. This is also reasonable and is an indicator that the user can remedy the situation by reenabling units as desired. However, if a device has only permanently disabled units, the "all units disabled" message is also (permanently) displayed. I believe that this is confusing, as it implies that the user can enable one or more of the units, as can be done for a device having user-disabled units. The unit count is already suppressed for a device having only permanently disabled units. I think the "all units disabled" message should be suppressed for this case as well. I have attached a modified "scp.c" that implements this minor change in the "show_device" routine. I would also like to request the addition of hooks in SCP that allow me to add device clone support and expanded MTABs to my extension module. For cloning, I just need a void pointer added to the end of the DEVICE structure. For expanded modifiers, I need a void pointer added to the end of the MTAB structure, as well as declaration of a hook function pointer that, if set by the VM, is called in the five places in SCP where modifiers are set or shown. These changes will have no effect on current simulators that do not fill in the hook pointers. I've attached a ZIP containing modified "scp.c", "scp.h", and "sim_defs.h" files from the V3.12-5 distribution that implement these changes, along with a "diff" text file that shows them. Writeups describing the changes are also included. Please let me know if you disagree with anything or if I may clarify. ================ SAVE and RESTORE ================ The SAVE and RESTORE commands present some challenges. Nominally, the commands will automatically save and restore the contents of main memory, all device and unit parameters, and all register values. This is good but incomplete. To save and restore the complete state, we must address: 1. Additional device instances. 2. Block saves of state variables. For problem 1, while SAVE will save the device and unit parameters from device clones, RESTORE will not recreate them. If they are recreated first, though, then RESTORE will work properly. To get around this, we can intercept the SAVE and RESTORE commands in the extensions module, duplicating the code in SCP that calls "sim_save" and "sim_rest", and inserting our own code that saves and restores the multiple-instance configuration. When saving, we call "sim_save" to write the existing save data. Then we call "ftell" to get the file position at the end of the save data. We then scan the "sim_devices" array to determine the instance count for all cloneable devices, including those without current clones, i.e., COUNT=1. We write these to the end of the save data along with a LF terminator. Then we write the saved file position and an 0xFF terminator, close the file, and return. When restoring, we "fseek" to the end of the file, offset back by the size of the file position variable (long int) and terminator (char), and read them. If the terminator is LF, then we are reading a "standard" SAVE file, so call "sim_rest" and return. If the terminator is 0xFF, then we "fseek" to the retrieved file position, which is the start of our instance data. We then read the instance data and set each cloneable device to the correct number of instances, which may be more or less than currently configured, until the LF terminator is seen. (The "ex_set_dev_count" routine may want to be split into a parsing routine and an action routine so we can call the latter without having to reconstruct SET COUNT command lines.) Then we "fseek" back to the start of the file and call "sim_rest". Restoration automatically stops after the original save data is processed, so upon return we simply close the file and return. The nice thing about this scheme is no changes to SCP are needed (except that "sim_save" and "sim_rest" need to be exported in "scp.h"). As any multiple device instances are created or reused within the current process, all pointers are valid (for the moment). Problem 2 exists for two reasons. First, if the RESTORE is done on a different simulator version or even on the same version compiled with a different compiler or with different compiler options, the layout or alignment of the fields within the block structure may have changed. A block save using the SVDATA register macro works by saving the block as though it were a byte array of a certain size. If the size or layout changes, it can't be detected by the RESTORE, and the restored aggregate variable will contain one or more invalid values (if the saved block is smaller than the restored block) or will overwrite whatever follows in memory (if the saved block is larger than the restored block). Second, even if the aggregates are identical in layout, if the variable contains pointers, they may be invalid when restored. Pointers to static variables or functions may change locations if the RESTORE is done on a different simulator executable as the targets may have moved. Even with the same executable, pointers within state variables that point to dynamically allocated structures, such as the delay times pointers in the LPA devices and the device back-pointers in the DC devices, may be invalid when restored, as the reallocated blocks may not be at the same addresses as when the blocks were saved. Note that the pointers will be valid after the cloned devices are recreated during the first part of the RESTORE, but then these valid pointers will be overwritten by the invalid pointers stored in the SVDATA register value. For this to work correctly, either block saves must be abandoned and replaced with saves of individual aggregate fields (e.g., the DIB, which contains constant pointers to the IMB and IOP handler functions, would have to be saved and restored by registering the individual numeric variables), or SAVE and RESTORE would have to be restricted to the same simulator executable (so that aggregate size and layout are identical), and block restores would have to be followed by restoration of all affected pointers. Individual variable saves won't work in the case of the DC device. The state structure contains an array of device structures, each of which contain an array of unit structures, each of which contains two data pointers. The device structures would have to be individually enumerated in order to specify the fields with the unit structures, as arrays of structures only handle single subscripts. So block saves are required, which in turn requires the executable restriction. With this restriction, block saves can be used without adjustment, even when they contain pointers, as long as they do not contain pointers to dynamic allocations. In practice, this means that only multiple-instance devices would need to have their pointers reestablished. Because we would need to extend the SCP routines anyway, RESTORE could be followed by calls to device adjust routines, which could detect the restoration condition by looking for SIM_SW_REST in the "sim_switches" global. In some cases, pointers could be eliminated in block data structures, e.g., by copying selected delay times into a "current times" structure, rather than pointing at the selected times structure within the state variable. Otherwise, the pointers would have to be reestablished. Note that the HP disc and tape libraries also have pointers in their controller state structures. So Problem 2 boils down to: - For single-instance devices, SAVE and RESTORE all static variables either individually or en bloc, - For multiple-instance devices, SAVE and RESTORE all static variables individually where possible, or en bloc where no pointers or only function pointers are present, or en bloc with post-RESTORE adjustment where dynamic data pointers are present. Restricting SAVE and RESTORE to the same executable may not be a hardship, and indeed the User's Guide has carried this warning for some time. The other two are more trouble, although using command files to perform the actions might mitigate this somewhat. It would still be awkward and probably is not worth the effort, given that practically no one uses these commands. ------------------------ Operational Restrictions ------------------------ SAVE and RESTORE have several restrictions that must be followed for it to work properly: 1. Identical executables must be used for both. This is so that register block saves may be used instead of individual structure field registers. 2. All writable media files must be saved and restored externally. Media files are not saved with the simulator state file, so their state should be identical at the SAVE and RESTORE points. This is accomplished either by exiting the simulator immediately after a SAVE and performing a single RESTORE, or by copying the media files to backup files at the SAVE point and copying them back prior to each restoration. 3. All terminal sessions will be disconnected upon restoration. This has several implications: - Buffered Telnet data that has not been retrieved by the multiplexer library will be lost. - Non-dataset ports will maintain their user sessions, so the order in which reconnections are performed is significant. - Dataset ports will be disconnected immediately upon resumption from restoration unless reconnection is performed after the RESTORE and before the CONTINUE (with SET CONNECT commands). 4. SAVE must be executed when the target system is (pretty much) quiescent. The CPU state during a cold-load or -store is not saved, so a SAVE occurring there will fail after the RESTORE. CPU: 2. // need to add 8 local static variables from "halt_mode_interrupt" // or move them into CPU_STATE structure and add that here. IMB: // would need a DEVICE to have REGs to store global state // if so, then trace_imb can be local via DIB // OR could save them as part of the CPU state // (local state is rebuilt at init time) NOTE: SAVE and RESTORE does not update MPE clock, which can only be set at cold load time!