OpenPFC / Build a simulation

Give measured work a name

Record real Euler updates with nested scopes, a custom scalar metric and a collective JSON export.

Download runnable lesson (.zip)

Includes the model, inputs, required headers and a short run guide.

A useful profiling report connects measured time with identifiable work. This refactors the synthetic spin demonstration into four Euler updates of \(u_t=-u\) on a small vector. Each rank computes its own identical vector; this is a profiling-contract example, not a distributed speedup experiment.

Nest regions around actual operations

for (int step = 0; step < steps; ++step) {
  profiling::ProfilingContextScope context(&session);
  profiling::openpfc_begin_frame_with_step_and_rank(session, step, comm.rank());
  OPENPFC_PROFILE("step") {
    OPENPFC_PROFILE("step/rhs") {
      for (int i = 0; i < n; ++i) du[i] = -u[i];
    }
    OPENPFC_PROFILE("step/update") {
      for (int i = 0; i < n; ++i) u[i] += dt * du[i];
    }
  }
  {
    profiling::ProfilingManualScope timer;
    timer.restart("diagnostics/norm");
    double norm2 = 0;
    for (double value : u) norm2 += value * value;
    session.set_frame_metric("rms", std::sqrt(norm2 / n));
    timer.stop();
  }
  profiling::openpfc_end_frame_memory_only_wall_from_clock(session, 0u, 0u, 0u);
}

step encloses step/rhs and step/update. Inclusive time contains child regions; exclusive time removes their contribution. A manual scope records the norm calculation separately. The session explicitly registers the custom rms frame metric before setting it. Unknown scalar metric names would otherwise be ignored. The three memory counters passed to the frame helper are zero because this example does not measure memory usage; they are not evidence of zero allocations.

Four frames mean four recorded outer iterations. The report’s call count reflects frames in which a path recorded time, not a general-purpose count of every invocation of an arbitrarily repeated inner scope. Repeated work within a frame contributes to its accumulated path time. The implicit diagnostics parent groups the measured norm child; it has no enclosing timed scope of its own. Its zero time and placeholder count must not be interpreted as a measured outer-region invocation.

Export and print collectively

profiling::ProfilingExportOptions output;
output.json_path = "results/profile.json";
output.run_id = "teaching";
output.export_metadata = {{"equation", "u_t=-u"}, {"dt", dt}};
session.finalize_and_export(comm, output); // Every rank participates.
profiling::ProfilingPrintOptions format;
format.ascii_lines = true;
format.show_exclusive_column = true;
format.mpi_aggregate_stdout = true;
profiling::print_profiling_timer(std::cout, comm, session, format);

Every rank participates in the export and aggregate report. The JSON is results/profile.json, with schema version 3, a teaching run identifier and equation metadata. Each rank’s frames remain separately identifiable. The default console statistic combines per-rank totals as documented by the profiling interface; it is not an additional elapsed wall-clock timer.

Keep numerical and timing checks separate

openpfc run model.cpp input.json --launcher=none
openpfc run model.cpp input.json --ranks=2 --launcher=mpirun

The empty input is unused. The numerical oracle after \(m\) updates is \(u=(1-.01)^m\). Tests check the final recurrence, all four RMS metrics on every rank, the exported frame counts and region hierarchy, and finite nonnegative recorded durations. No duration is promoted to a benchmark claim. Timing instrumentation also has overhead, especially for such tiny regions; use larger representative work when investigating performance. See the profiling session contract and the handbook’s profiling guide for larger applications.

The complete C++ model and starting input are available with this lesson.