Observability of the Java Virtual Machine

Image
The JVM is one of the most observable runtimes. It provides us lots of tools for troubleshooting a JVM application in production. 1. Thread observability Threads are how the JVM actually does work. When something is wrong in production, the symptom is almost always a thread: stopped, blocked, leaking etc. Thread dumps work on any JVM with no  instrumentation, no agents, no restarts. <Example project link with /threaddump endpoint>         // (1) Deadlock — two threads grab the same pair of locks in opposite order.         new Thread(() -> grab(LOCK_A, LOCK_B), "deadlock-A-then-B").start();         new Thread(() -> grab(LOCK_B, LOCK_A), "deadlock-B-then-A").start(); http://localhost:8080/actuator/threaddump To list the JVMS, we can use the command below. PS C:\observe-jvm> jps -lv 25296 jdk.jcmd/sun.tools.jps.Jps -Dapplication.home=C:\Program Files\Microsoft\jdk-21.0.3.9-hotspot -Xms8m -Djdk.module.main=...

Understanding SimpleScalar codebase

Among others, sim-safe is the most user-friendly simulator and the implementation is crafted for clarity.

Generally there are two types of simulators:
  • Functional simulators
    • implement architecture (what programmers see)
    • perform the actual execution
    • sim-fast, sim-safe, sim-profile, sim-cache
  • Performance simulators
    • implement micro-architecture
    • model system internals
    • measure time
    • sim-outorder
Question: What is Dlite debugger? How can we debug a simulator??

DLite is a symbolic debugger. To use it, start the simulator with -i option
Then user the debugger commands, for example: "step"

Question: To work with TLB, do I have to fully understand sim-outorder ??

http://www.simplescalar.com/docs/simple_tutorial_v2.pdf
page:14

http://www.simplescalar.com/docs/hack_guide_v2.pdf

Question:
How does sim-safe count number of instructions?
Where is iTLB related code?

In main.c file:

/* options database*/
struct opt_odb_t *sim_odb;

/* simulated registers */
static struct regs_t regs;

/* simulated memory */
static struct mem_t *mem = NULL;

/* stats database */
struct stat_sdb_t *sim_sdb;

/* execution instruction counter */
counter_t sim_num_insn = 0;

/* track number of refs */
static counter_t sim_num_refs = 0; 

First of all, all the options are written to an options database. There are struct representations for registers and memory. Statistics and counters are also defined as global variables.

int main(int argc, char **argv, char **envp)
{
/* register global options */
sim_odb = opt_new();

/* register all simulator-specific options */
sim_reg_options(sim_odb);

/* initialize all simulation modules */
sim_init();

/* initialize architected state */
sim_load_prog(argv[exec_index], argc - exec_index, argv + exec_index, envp);

/* register all simulator stats */
sim_sdb = stat_new();
sim_reg_stats(sim_sdb);

After filling the variables for global and simulator specific options, the simulation module is initialized. This means, the data structures for register and memory representations are created. After that, the most important step is reading the binary file and putting that ECOFF file to related memory structure:

/* initialize architected state */
sim_load_prog(argv[exec_index], argc - exec_index, argv + exec_index, envp);

Loading the binary is done by "loader.c" file. This process is complicated and related to the internals of ECOFF file format, which is beyond the scope of our research.

/* load the program into memory, try both endians */
#if defined(__CYGWIN32__) || defined(_MSC_VER)
fobj = fopen(argv[0], "rb");
#else
fobj = fopen(argv[0], "r");
#endif
if (!fobj) {
fprintf(stderr, "cannot open executable");

exit(1);
}

if (fread(&fhdr, sizeof(struct ecoff_filehdr), 1, fobj) < 1) {

fprintf(stderr, "cannot read header from executable");

exit(1);
}

Then we go on by creating structures for statistical data.

stat_reg_counter(sdb, "sim_num_insn",
"total number of instructions executed",
&sim_num_insn, sim_num_insn, NULL);
stat_reg_counter(sdb, "sim_num_refs",
"total number of loads and stores executed",
&sim_num_refs, 0, NULL);
stat_reg_int(sdb, "sim_elapsed_time",
"total simulation time in seconds",

&sim_elapsed_time, 0, NULL);

After the preparation, now it's time to call the simulator main function:

/* record start of execution time, used in rate stats */
sim_start_time = time((time_t *)NULL);

/* output simulation conditions */
char *s = ctime(&sim_start_time);

fprintf(stderr, "\nsim: simulation started @ %s, options follow:\n", s);

sim_main();

Now let's have a look at sim-safe.c file:


/* start simulation, program loaded, processor precise state initialized */
void
sim_main(void)
{

fprintf(stderr, "sim: ** starting functional simulation **\n");

/* set up initial default next PC */
regs.regs_NPC = regs.regs_PC + sizeof(md_inst_t);

while (TRUE)
{

/* get the next instruction to execute */
MD_FETCH_INST(inst, mem, regs.regs_PC);

/* keep an instruction count */
sim_num_insn++;


/* decode the instruction */
MD_SET_OPCODE(op, inst);

/* execute the instruction */
switch (op)
{
#define DEFINST(OP,MSK,NAME,OPFORM,RES,FLAGS,O1,O2,I1,I2,I3) \
case OP: \
          SYMCAT(OP,_IMPL); \
          break;

/* Additional case statements..
}

/* go to the next instruction */
regs.regs_PC = regs.regs_NPC;
regs.regs_NPC += sizeof(md_inst_t);

/* finish early? */
if (max_insts && sim_num_insn >= max_insts)

return;
}
}

As the comment indicates, before the simulator function is called, the program is loaded and the processor is initialized. Inside the main loop, the next instruction is fetched, counted, decoded and executed. Then we go to the next instruction if the maximum instruction limit is not reached.

Comments

Popular posts from this blog

The WeakReference class, monitoring memory leak and garbage collection in a Java application

Simplescalar Simulator - Part 2: sim-outorder.c

Notes on Java Performance