From bdf00966f9a3e6ab630b39065832ce636c933cd2 Mon Sep 17 00:00:00 2001 From: Yuval Tassa Date: Tue, 19 May 2026 08:27:35 -0700 Subject: [PATCH] Add compiler timing diagnostics to mjsCompiler, printed by `compile.cc` For example, `compile mujoco_menagerie/robotis_op3/scene.xml` now outputs ``` Compile 1 (cold cache): total: 317.2 ms assets: 284.8 ms (wall clock) load: 616.1 ms hull: 26.4 ms poly: 137.8 ms inert: 177.8 ms bvh: 568.2 ms octr: 1.6 ms tex: 25.3 ms other: 32.4 ms Compile 2 (warm cache): total: 79.9 ms assets: 54.5 ms (wall clock) load: 888.5 ms hull: 0.0 ms poly: 0.0 ms inert: 0.0 ms bvh: 0.0 ms octr: 0.0 ms tex: 21.4 ms other: 25.3 ms ``` PiperOrigin-RevId: 917850214 Change-Id: Iaec86230bec0faf2e47820e20cbff61de5b2621e --- doc/APIreference/APItypes.rst | 12 +++++ doc/APIreference/functions.rst | 9 ++++ doc/changelog.rst | 4 ++ doc/includes/references.h | 17 +++++++ doc/programming/samples.rst | 13 +++-- include/mujoco/mjspec.h | 18 +++++++ include/mujoco/mujoco.h | 3 ++ python/mujoco/introspect/enums.py | 17 +++++++ python/mujoco/introspect/functions.py | 16 ++++++ python/mujoco/specs.cc | 8 +++ python/mujoco/specs_test.py | 18 +++++++ sample/compile.cc | 73 +++++++++++++++++---------- src/user/user_api.cc | 11 ++++ src/user/user_mesh.cc | 29 +++++++++-- src/user/user_model.cc | 36 ++++++++++--- src/user/user_model.h | 1 + src/user/user_objects.h | 2 + test/user/user_api_test.cc | 27 ++++++++++ unity/Runtime/Bindings/MjBindings.cs | 12 +++++ wasm/codegen/generated/bindings.cc | 17 +++++++ wasm/codegen/generators/constants.py | 1 + wasm/codegen/generators/structs.py | 4 ++ wasm/codegen/templates/bindings.cc | 4 ++ 23 files changed, 309 insertions(+), 43 deletions(-) diff --git a/doc/APIreference/APItypes.rst b/doc/APIreference/APItypes.rst index b5fde31f..6fe1c6c5 100644 --- a/doc/APIreference/APItypes.rst +++ b/doc/APIreference/APItypes.rst @@ -828,6 +828,18 @@ Type of built-in procedural mesh. .. mujoco-include:: mjtMeshBuiltin +.. _mjtCTimer: + +mjtCTimer +~~~~~~~~~ + +Compiler timing categories, used in :ref:`mjs_getTimer`. Top-level timers (``TOTAL``, ``ASSETS``) measure wall-clock +time. Asset sub-timers measure CPU time summed across all assets; with multi-threaded compilation their sum can exceed +the ``ASSETS`` wall-clock time. + +.. mujoco-include:: mjtCTimer + + .. _tyPluginEnums: Plugins diff --git a/doc/APIreference/functions.rst b/doc/APIreference/functions.rst index fc8a07c4..298e5134 100644 --- a/doc/APIreference/functions.rst +++ b/doc/APIreference/functions.rst @@ -2079,6 +2079,15 @@ Write [datetime, type: message] to MUJOCO_LOG.TXT. Get compiler error message from spec. +.. _mjs_getTimer: + +`mjs_getTimer <#mjs_getTimer>`__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. mujoco-include:: mjs_getTimer + +Get compiler timing diagnostics from spec, returns pointer to array of size mjNCTIMER. + .. _mjs_isWarning: `mjs_isWarning <#mjs_isWarning>`__ diff --git a/doc/changelog.rst b/doc/changelog.rst index 93d6d365..ea44b45a 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -17,6 +17,10 @@ General release, now uses a fixed seed. The previous implementation seeded with ``mjData.time``, which introduced subtle yet undesirable time dependence. - Flexes are now allowed to sleep, with the exception of completely passive (constraint-free) flexes. +- Added compiler timing diagnostics via the new :ref:`mjtCTimer` enum and the :ref:`mjs_getTimer` C API. After + :ref:`mj_compile`, per-category timings (total, assets, mesh loading, convex hull, normals, inertia, BVH, octree, + textures) are available via ``mjs_getTimer(spec)``. The :ref:`compile` sample prints a detailed timing + breakdown when run without an output file. .. admonition:: Breaking API changes :class: attention diff --git a/doc/includes/references.h b/doc/includes/references.h index 84d244c4..cc3d6b49 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -1965,6 +1965,22 @@ typedef enum mjtOrientation_ { // type of orientation specifier mjORIENTATION_ZAXIS, // z axis (minimal rotation) mjORIENTATION_EULER, // Euler angles } mjtOrientation; +typedef enum mjtCTimer_ { // compiler timing categories + // top-level timers (wall-clock) + mjCTIMER_TOTAL = 0, // total compile time + mjCTIMER_ASSETS, // asset compilation + + // asset sub-timers (CPU time, summed across all assets) + mjCTIMER_TEXTURE, // textures + mjCTIMER_MESH_LOAD, // mesh: file loading + mjCTIMER_MESH_HULL, // mesh: convex hull + mjCTIMER_MESH_POLYGON, // mesh: normals and polygons + mjCTIMER_MESH_INERTIA, // mesh: volume, CoM, inertia + mjCTIMER_MESH_BVH, // mesh: bounding volume hierarchy + mjCTIMER_MESH_OCTREE, // mesh: octree and SDF + + mjNCTIMER // number of compiler timers +} mjtCTimer; typedef struct mjsElement_ { // element type, do not modify mjtObj elemtype; // element type uint64_t signature; // compilation signature @@ -3471,6 +3487,7 @@ void mju_free(void* ptr); void mj_warning(mjData* d, int warning, int info); void mju_writeLog(const char* type, const char* msg); const char* mjs_getError(mjSpec* s); +const double* mjs_getTimer(mjSpec* s); int mjs_isWarning(mjSpec* s); void mju_zero3(mjtNum res[3]); void mju_copy3(mjtNum res[3], const mjtNum data[3]); diff --git a/doc/programming/samples.rst b/doc/programming/samples.rst index 263ee833..8a964c86 100644 --- a/doc/programming/samples.rst +++ b/doc/programming/samples.rst @@ -120,16 +120,19 @@ Windows power plan so that the minimum processor state is 100%. `compile `_ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -This code sample evokes the built-in parser and compiler. It implements all possible model conversions from (MJCF, URDF, -MJB) format to (MJCF, MJB, TXT) format. Models saved as MJCF use a canonical subset of our format as described in the -:doc:`../modeling` chapter, and therefore MJCF-to-MJCF conversion will generally result in a different file. +This code sample invokes the built-in parser and compiler. It implements all possible model conversions from (MJCF, +URDF, MJB) format to (MJCF, MJB, TXT) format. Models saved as MJCF use a canonical subset of our format as described in +the :doc:`../modeling` chapter, and therefore MJCF-to-MJCF conversion will generally result in a different file. The TXT format is a human-readable road-map to the model. It cannot be loaded by MuJoCo, but can be a very useful aid during model development. It is in one-to-one correspondence with the compiled mjModel. Note also that one can use the function :ref:`mj_printData` to create a text file which is in one-to-one correspondence with mjData, although this is not done by the code sample. -If the input file is MJCF and the output file is empty, compilation is performed and timed twice to measure the impact -of the compiler's :ref:`asset cache`. +If the input file is MJCF or URDF and the output file is empty, compilation is performed twice to measure the impact +of the compiler's :ref:`asset cache`. A detailed timing breakdown is printed for each compilation, showing +total time, asset processing time (wall clock), and per-category CPU times for meshes and textures. These timings are +read from the :ref:`mjtCTimer` fields via :ref:`mjs_getTimer`, which can be read programmatically +after any call to :ref:`mj_compile`. .. _saBasic: diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 3b1ca49d..ab1b2447 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -129,6 +129,24 @@ typedef enum mjtOrientation_ { // type of orientation specifier } mjtOrientation; +typedef enum mjtCTimer_ { // compiler timing categories + // top-level timers (wall-clock) + mjCTIMER_TOTAL = 0, // total compile time + mjCTIMER_ASSETS, // asset compilation + + // asset sub-timers (CPU time, summed across all assets) + mjCTIMER_TEXTURE, // textures + mjCTIMER_MESH_LOAD, // mesh: file loading + mjCTIMER_MESH_HULL, // mesh: convex hull + mjCTIMER_MESH_POLYGON, // mesh: normals and polygons + mjCTIMER_MESH_INERTIA, // mesh: volume, CoM, inertia + mjCTIMER_MESH_BVH, // mesh: bounding volume hierarchy + mjCTIMER_MESH_OCTREE, // mesh: octree and SDF + + mjNCTIMER // number of compiler timers +} mjtCTimer; + + //-------------------------------- attribute structs (mjs) ----------------------------------------- typedef struct mjsElement_ { // element type, do not modify diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 6bf94284..ab41686d 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -1005,6 +1005,9 @@ MJAPI void mju_writeLog(const char* type, const char* msg); // Get compiler error message from spec. MJAPI const char* mjs_getError(mjSpec* s); +// Get compiler timing diagnostics from spec, returns pointer to array of size mjNCTIMER. +MJAPI const double* mjs_getTimer(mjSpec* s); + // Return 1 if compiler error is a warning. MJAPI int mjs_isWarning(mjSpec* s); diff --git a/python/mujoco/introspect/enums.py b/python/mujoco/introspect/enums.py index 6518b2dc..7de73447 100644 --- a/python/mujoco/introspect/enums.py +++ b/python/mujoco/introspect/enums.py @@ -718,6 +718,23 @@ ENUMS: Mapping[str, EnumDecl] = dict([ ('mjORIENTATION_EULER', 4), ]), )), + ('mjtCTimer', + EnumDecl( + name='mjtCTimer', + declname='enum mjtCTimer_', + values=dict([ + ('mjCTIMER_TOTAL', 0), + ('mjCTIMER_ASSETS', 1), + ('mjCTIMER_TEXTURE', 2), + ('mjCTIMER_MESH_LOAD', 3), + ('mjCTIMER_MESH_HULL', 4), + ('mjCTIMER_MESH_POLYGON', 5), + ('mjCTIMER_MESH_INERTIA', 6), + ('mjCTIMER_MESH_BVH', 7), + ('mjCTIMER_MESH_OCTREE', 8), + ('mjNCTIMER', 9), + ]), + )), ('mjtCatBit', EnumDecl( name='mjtCatBit', diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index 882c323f..53061ac2 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -6471,6 +6471,22 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ ), doc='Get compiler error message from spec.', )), + ('mjs_getTimer', + FunctionDecl( + name='mjs_getTimer', + return_type=PointerType( + inner_type=ValueType(name='double', is_const=True), + ), + parameters=( + FunctionParameterDecl( + name='s', + type=PointerType( + inner_type=ValueType(name='mjSpec'), + ), + ), + ), + doc='Get compiler timing diagnostics from spec, returns pointer to array of size mjNCTIMER.', # pylint: disable=line-too-long + )), ('mjs_isWarning', FunctionDecl( name='mjs_isWarning', diff --git a/python/mujoco/specs.cc b/python/mujoco/specs.cc index c5a60b52..abe101d9 100644 --- a/python/mujoco/specs.cc +++ b/python/mujoco/specs.cc @@ -55,6 +55,7 @@ using MjDouble3 = Eigen::Map; using MjDouble4 = Eigen::Map; using MjDouble5 = Eigen::Map>; using MjDouble6 = Eigen::Map>; +using MjDouble9 = Eigen::Map>; using MjDouble10 = Eigen::Map>; using MjDouble11 = Eigen::Map>; using MjDoubleVec = Eigen::Map; @@ -69,6 +70,7 @@ using MjDoubleRef3 = Eigen::Ref; using MjDoubleRef4 = Eigen::Ref; using MjDoubleRef5 = Eigen::Ref>; using MjDoubleRef6 = Eigen::Ref>; +using MjDoubleRef9 = Eigen::Ref>; using MjDoubleRef10 = Eigen::Ref>; using MjDoubleRef11 = Eigen::Ref>; using MjDoubleRefVec = Eigen::Ref; @@ -461,6 +463,12 @@ PYBIND11_MODULE(_specs, m) { mjSpec.def_property_readonly("_address", [](const MjSpec& self) { return reinterpret_cast(self.ptr); }); + mjSpec.def_property_readonly( + "timer", + [](MjSpec& self) -> MjDouble9 { + return MjDouble9(const_cast(mjs_getTimer(self.ptr))); + }, + py::return_value_policy::reference_internal); mjSpec.def_property( "copy_during_attach", [](MjSpec& self) { diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index fe3f45b1..7e1baa04 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -47,6 +47,24 @@ class SpecsTest(absltest.TestCase): self.assertIsInstance(spec.worldbody, mujoco.MjsBody) self.assertIsInstance(spec.worldbody, typing.get_args(mujoco.MjStruct)) + def test_timer(self): + xml = """ + + + + + + + + + + """ + spec = mujoco.MjSpec.from_string(xml) + model = spec.compile() + self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TOTAL], 0) + self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_ASSETS], 0) + self.assertGreater(spec.timer[mujoco.mjtCTimer.mjCTIMER_TEXTURE], 0) + def test_basic(self): # Create a spec. spec = mujoco.MjSpec() diff --git a/sample/compile.cc b/sample/compile.cc index 97974dd3..f39e13b9 100644 --- a/sample/compile.cc +++ b/sample/compile.cc @@ -25,11 +25,11 @@ // help static constexpr char helpstring[] = - "\n Usage: compile infile outfile\n" + "\n Usage: compile infile [outfile]\n" " infile can be in mjcf, urdf, mjb format\n" - " outfile can be in mjcf, mjb, txt format, or empty\n\n" - " if infile is mjcf and outfile is empty, compilation will be " - "timed twice to measure the impact of caching\n\n" + " outfile can be in mjcf, mjb, txt format\n\n" + " if infile is mjcf or urdf and outfile is omitted, a detailed\n" + " timing breakdown is printed for two compilations (cold and warm cache)\n\n" " Example: compile model.xml [model.mjb]\n"; @@ -137,17 +137,45 @@ int main(int argc, char** argv) { } } + // print compiler timing diagnostics + auto print_timers = [](const mjSpec* s, const char* label) { + const double* timer = mjs_getTimer(const_cast(s)); + std::printf("\n%s:\n", label); + std::printf(" total: %8.1f ms\n", 1e3 * timer[mjCTIMER_TOTAL]); + std::printf(" assets: %8.1f ms (wall clock)\n", 1e3 * timer[mjCTIMER_ASSETS]); + std::printf(" load: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_LOAD]); + std::printf(" hull: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_HULL]); + std::printf(" poly: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_POLYGON]); + std::printf(" inert: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_INERTIA]); + std::printf(" bvh: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_BVH]); + std::printf(" octr: %8.1f ms\n", 1e3 * timer[mjCTIMER_MESH_OCTREE]); + std::printf(" tex: %8.1f ms\n", 1e3 * timer[mjCTIMER_TEXTURE]); + std::printf(" other: %8.1f ms\n", + 1e3 * (timer[mjCTIMER_TOTAL] - timer[mjCTIMER_ASSETS])); + }; + // load model - double first=0, second=0; + mjSpec* s = nullptr; if (type1==typeXML) { - double starttime = gettm(); - m = mj_loadXML(argv[1], 0, error, 1000); - first = gettm() - starttime; - if (m && type2 == typeNONE) { + s = mj_parseXML(argv[1], 0, error, 1000); + if (!s) { + return finish(error, EXIT_FAILURE); + } + + m = mj_compile(s, 0); + if (!m) { + mj_deleteSpec(s); + return finish("Could not compile model", EXIT_FAILURE); + } + + print_timers(s, "Compile 1 (cold cache)"); + + if (type2 == typeNONE) { mj_deleteModel(m); - starttime = gettm(); - m = mj_loadXML(argv[1], 0, error, 1000); - second = gettm() - starttime; + m = mj_compile(s, 0); + if (m) { + print_timers(s, "Compile 2 (warm cache)"); + } } } else { m = mj_loadModel(argv[1], 0); @@ -155,16 +183,14 @@ int main(int argc, char** argv) { // check error if (!m) { - if (type1 == typeXML) { - return finish(error, EXIT_FAILURE); - } else { - return finish("Could not load model", EXIT_FAILURE); - } + if (s) mj_deleteSpec(s); + return finish("Could not load model", EXIT_FAILURE); } // save model if (type2 == typeXML) { if (!mj_saveLastXML(argv[2], m, error, 1000)) { + if (s) mj_deleteSpec(s); return finish(error, EXIT_FAILURE, m); } } else if (type2 == typeMJB) { @@ -174,15 +200,6 @@ int main(int argc, char** argv) { } // finalize - char msg[1000]; - if (first && type2 == typeNONE) { - snprintf(msg, sizeof(msg), "Done.\n" - "First compile: %.4gs\n" - "Second compile: %.4gs", - first, second); - } else { - snprintf(msg, sizeof(msg), "Done."); - } - - return finish(msg, EXIT_SUCCESS, m); + if (s) mj_deleteSpec(s); + return finish("\nDone.", EXIT_SUCCESS, m); } diff --git a/src/user/user_api.cc b/src/user/user_api.cc index fe25fae2..d423b93c 100644 --- a/src/user/user_api.cc +++ b/src/user/user_api.cc @@ -427,6 +427,17 @@ const char* mjs_getError(mjSpec* s) { +// get compiler timers from model +const double* mjs_getTimer(mjSpec* s) { + if (!s) { + return nullptr; + } + mjCModel* modelC = static_cast(s->element); + return modelC->timer; +} + + + // check if model has warnings int mjs_isWarning(mjSpec* s) { mjCModel* modelC = static_cast(s->element); diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index d161bd5a..fa08d766 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -14,19 +14,18 @@ #include #include +#include #include #include #include #include #include #include -#include #include #include #include #include #include -#include #include #include #include @@ -688,11 +687,17 @@ void mjCMesh::Compile(const mjVFS* vfs) { // compiler void mjCMesh::TryCompile(const mjVFS* vfs) { + using Clock = std::chrono::steady_clock; + using Seconds = std::chrono::duration; + std::fill_n(mesh_timer_, mjNCTIMER, 0.0); + bool fromCache = false; CopyFromSpec(); visual_ = true; mjCCache *cache = reinterpret_cast(mj_getCache()->impl_); + Clock::time_point t0 = Clock::now(); + // load file if (!file_.empty()) { vert_.clear(); @@ -765,12 +770,13 @@ void mjCMesh::TryCompile(const mjVFS* vfs) { LoadSDF(); // create using marching cubes } + mesh_timer_[mjCTIMER_MESH_LOAD] = Seconds(Clock::now() - t0).count(); + CheckInitialMesh(); // compute mesh properties if (!fromCache) { Process(); - if (!file_.empty()) { CacheMesh(cache, resource_); } @@ -778,6 +784,7 @@ void mjCMesh::TryCompile(const mjVFS* vfs) { // When a mesh is loaded from the cache, has no octree but needs one, // we need to compute it here. If inversely it has an octree but we *do not* // need one, we clear it. + t0 = Clock::now(); if (!needsdf) { octree_.Clear(); } else if (octree_.NumNodes() == 0) { @@ -789,6 +796,7 @@ void mjCMesh::TryCompile(const mjVFS* vfs) { octree_.ComputeSdfCoeffs(dvert.data(), nvert(), face_.data(), nface(), tree_); } } + mesh_timer_[mjCTIMER_MESH_OCTREE] = Seconds(Clock::now() - t0).count(); } // close resource @@ -1341,7 +1349,9 @@ double mjCMesh::ComputeFaceCentroid(double facecen[3], const double* dvert) cons void mjCMesh::Process() { std::vector dvert(vert_.begin(), vert_.end()); - + using Clock = std::chrono::steady_clock; + using Seconds = std::chrono::duration; + Clock::time_point t0; // create half-edge structure (if mesh was in XML) if (halfedge_.empty()) { for (int i = 0; i < nface(); i++) { @@ -1370,6 +1380,7 @@ void mjCMesh::Process() { } } + t0 = Clock::now(); // make graph describing convex hull if (needhull_ || face_.empty()) { MakeGraph(dvert.data()); @@ -1379,7 +1390,9 @@ void mjCMesh::Process() { if (face_.empty()) { CopyGraph(); } + mesh_timer_[mjCTIMER_MESH_HULL] += Seconds(Clock::now() - t0).count(); + t0 = Clock::now(); // no normals: make if (normal_.empty()) { MakeNormal(dvert.data()); @@ -1424,6 +1437,9 @@ void mjCMesh::Process() { } } + mesh_timer_[mjCTIMER_MESH_POLYGON] += Seconds(Clock::now() - t0).count(); + + t0 = Clock::now(); // user offset, rotation, scaling ApplyTransformations(dvert.data()); @@ -1519,7 +1535,9 @@ void mjCMesh::Process() { // recompute polygon normals MakePolygonNormals(dvert.data()); + mesh_timer_[mjCTIMER_MESH_INERTIA] += Seconds(Clock::now() - t0).count(); + t0 = Clock::now(); // make bounding volume hierarchy if (tree_.Bvh().empty()) { face_aabb_.clear(); @@ -1530,7 +1548,9 @@ void mjCMesh::Process() { } tree_.CreateBVH(); } + mesh_timer_[mjCTIMER_MESH_BVH] += Seconds(Clock::now() - t0).count(); + t0 = Clock::now(); // make octree if (needsdf) { octree_.SetFace(dvert, face_); @@ -1546,6 +1566,7 @@ void mjCMesh::Process() { for (int i = 0; i < (int)dvert.size(); i++) { vert_[i] = (float)dvert[i]; } + mesh_timer_[mjCTIMER_MESH_OCTREE] += Seconds(Clock::now() - t0).count(); } diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 7d87f3b5..c9e38131 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -16,7 +16,7 @@ #include #include -#include +#include #include #include #include @@ -662,7 +662,6 @@ mjCModel& mjCModel::operator+=(mjCDef& subtree) { // remove default class from array mjCModel& mjCModel::operator-=(const mjCDef& subtree) { - // check we aren't trying to remove the 'main' default if (subtree.id == 0) { throw mjCError(0, "cannot remove the global default ('main')"); @@ -922,7 +921,7 @@ void mjCModel::ComputeSparseSizes() { // 1. build dof_parentid, dof_bodyid if (nbody > 0) { - body_lastdof_map[0] = -1; // world has no parent dof + body_lastdof_map[0] = -1; // world has no parent dof } for (int i = 0; i < nbody; ++i) { mjCBody* pb = bodies_[i]; @@ -961,7 +960,7 @@ void mjCModel::ComputeSparseSizes() { nD = 2 * nM - nv; // 4. compute subtreedofs and nB - for(int i = nbody - 1; i >= 0; --i) { + for (int i = nbody - 1; i >= 0; --i) { bodies_[i]->subtreedofs = bodies_[i]->dofnum; for (const auto* child : bodies_[i]->Bodies()) { bodies_[i]->subtreedofs += child->subtreedofs; @@ -984,7 +983,7 @@ void mjCModel::ComputeSparseSizes() { } // 5. compute nC - for(int i = 0; i < nbody; ++i) { + for (int i = 0; i < nbody; ++i) { mjCBody* pb = bodies_[i]; mjCBody* par = pb->parent; int parentid = par ? par->id : 0; @@ -4744,10 +4743,13 @@ static void CompileMesh(mjCMesh* mesh, const mjVFS* vfs, static void CompileTexture(mjCTexture* texture, const mjVFS* vfs, std::exception_ptr& exception, std::mutex& exception_mutex, std::string* warningtext) { + using Clock = std::chrono::steady_clock; + using Seconds = std::chrono::duration; local_warningtext_ptr = warningtext; auto previous_handler = _mjPRIVATE__get_tls_warning_fn(); _mjPRIVATE__set_tls_warning_fn(warninghandler); + Clock::time_point t0 = Clock::now(); try { texture->Compile(vfs); } catch (...) { @@ -4756,6 +4758,7 @@ static void CompileTexture(mjCTexture* texture, const mjVFS* vfs, exception = std::current_exception(); } } + texture->texture_time_ = Seconds(Clock::now() - t0).count(); _mjPRIVATE__set_tls_warning_fn(previous_handler); local_warningtext_ptr = nullptr; @@ -4843,6 +4846,15 @@ void mjCModel::CompileMeshesAndTextures(const mjVFS* vfs) { if (texture_exception) { std::rethrow_exception(texture_exception); } + + for (int i = 0; i < nmesh; i++) { + for (int t = 0; t < mjNCTIMER; t++) { + timer[t] += meshes_[i]->mesh_timer_[t]; + } + } + for (int i = 0; i < ntexture; i++) { + timer[mjCTIMER_TEXTURE] += textures_[i]->texture_time_; + } } // compute qpos0 @@ -4984,6 +4996,12 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { } disable_usethread(compiler.usethread); #endif + using Clock = std::chrono::steady_clock; + using Seconds = std::chrono::duration; + for (int i=0; i < mjNCTIMER; i++) { + timer[i] = 0; + } + Clock::time_point timer_start = Clock::now(); // check if nan test works double test = mjNAN; if (mjuu_defined(test)) { @@ -5085,7 +5103,11 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { SetNuser(); // compile meshes and textures (needed for geom compilation) - CompileMeshesAndTextures(vfs); + { + Clock::time_point t0 = Clock::now(); + CompileMeshesAndTextures(vfs); + timer[mjCTIMER_ASSETS] = Seconds(Clock::now() - t0).count(); + } // compile objects in kinematic tree for (int i=0; i < bodies_.size(); i++) { @@ -5359,6 +5381,8 @@ void mjCModel::TryCompile(mjModel*& m, mjData*& d, const mjVFS* vfs) { // save signature m->signature = Signature(); + timer[mjCTIMER_TOTAL] = Seconds(Clock::now() - timer_start).count(); + // special cases that are not caused by user edits if (compiler.fusestatic || compiler.discardvisual || !pairs_.empty() || !excludes_.empty()) { diff --git a/src/user/user_model.h b/src/user/user_model.h index 70cc39fc..6d554649 100644 --- a/src/user/user_model.h +++ b/src/user/user_model.h @@ -198,6 +198,7 @@ class mjCModel : public mjCModel_, private mjSpec { mjCModel& operator-=(const mjCDef& subtree); // remove default tree from this model mjSpec spec; + double timer[mjNCTIMER] = {0}; // compiler timers mjModel* Compile(const mjVFS* vfs = nullptr, mjModel** m = nullptr); // construct mjModel bool CopyBack(const mjModel*); // DECOMPILER: copy numeric back diff --git a/src/user/user_objects.h b/src/user/user_objects.h index ac58c810..cbe85e6b 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -1133,6 +1133,7 @@ class mjCMesh_ : public mjCBase { // octree mjCOctree octree_; // octree of the mesh + double mesh_timer_[mjNCTIMER] = {0}; }; class mjCMesh: public mjCMesh_, private mjsMesh { @@ -1465,6 +1466,7 @@ class mjCTexture : public mjCTexture_, private mjsTexture { void PointToLocal(void); void NameSpace(const mjCModel* m); void Compile(const mjVFS* vfs); + double texture_time_ = 0; std::string File() const { return file_; } std::string get_content_type() const { return content_type_; } diff --git a/test/user/user_api_test.cc b/test/user/user_api_test.cc index b26ebf87..707f698b 100644 --- a/test/user/user_api_test.cc +++ b/test/user/user_api_test.cc @@ -3378,5 +3378,32 @@ TEST_F(MujocoTest, UserValue) { mj_deleteSpec(spec); } +TEST_F(MujocoTest, CompilerTimers) { + static constexpr char xml[] = R"( + + + + + + + + + + )"; + std::array error; + mjSpec* spec = mj_parseXMLString(xml, 0, error.data(), error.size()); + ASSERT_THAT(spec, NotNull()) << error.data(); + + mjModel* model = mj_compile(spec, 0); + ASSERT_THAT(model, NotNull()); + + EXPECT_GT(mjs_getTimer(spec)[mjCTIMER_TOTAL], 0); + EXPECT_GT(mjs_getTimer(spec)[mjCTIMER_ASSETS], 0); + EXPECT_GT(mjs_getTimer(spec)[mjCTIMER_TEXTURE], 0); + + mj_deleteModel(model); + mj_deleteSpec(spec); +} + } // namespace } // namespace mujoco diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 8b66b5a5..164edc61 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -573,6 +573,18 @@ public enum mjtOrientation : int{ mjORIENTATION_ZAXIS = 3, mjORIENTATION_EULER = 4, } +public enum mjtCTimer : int{ + mjCTIMER_TOTAL = 0, + mjCTIMER_ASSETS = 1, + mjCTIMER_TEXTURE = 2, + mjCTIMER_MESH_LOAD = 3, + mjCTIMER_MESH_HULL = 4, + mjCTIMER_MESH_POLYGON = 5, + mjCTIMER_MESH_INERTIA = 6, + mjCTIMER_MESH_BVH = 7, + mjCTIMER_MESH_OCTREE = 8, + mjNCTIMER = 9, +} public enum mjtCatBit : int{ mjCAT_STATIC = 1, mjCAT_DYNAMIC = 2, diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index 21d0565c..c82c2de3 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -5612,6 +5612,7 @@ struct MjSpec { std::unique_ptr copy(); mjSpec* get() const; void set(mjSpec* ptr); + emscripten::val timer() const; mjString modelname() const { return (ptr_ && ptr_->modelname) ? *(ptr_->modelname) : ""; } @@ -8402,6 +8403,10 @@ MjSpec::~MjSpec() { mjSpec *MjSpec::get() const { return ptr_; } void MjSpec::set(mjSpec *ptr) { ptr_ = ptr; } +emscripten::val MjSpec::timer() const { + return emscripten::val(emscripten::typed_memory_view(9, mjs_getTimer(ptr_))); +} + std::unique_ptr mj_loadXML_wrapper_1(std::string filename) { char error[1000]; mjModel *model = mj_loadXML(filename.c_str(), nullptr, error, sizeof(error)); @@ -10911,6 +10916,17 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .value("mjBUTTON_LEFT", mjBUTTON_LEFT) .value("mjBUTTON_RIGHT", mjBUTTON_RIGHT) .value("mjBUTTON_MIDDLE", mjBUTTON_MIDDLE); + enum_("mjtCTimer") + .value("mjCTIMER_TOTAL", mjCTIMER_TOTAL) + .value("mjCTIMER_ASSETS", mjCTIMER_ASSETS) + .value("mjCTIMER_TEXTURE", mjCTIMER_TEXTURE) + .value("mjCTIMER_MESH_LOAD", mjCTIMER_MESH_LOAD) + .value("mjCTIMER_MESH_HULL", mjCTIMER_MESH_HULL) + .value("mjCTIMER_MESH_POLYGON", mjCTIMER_MESH_POLYGON) + .value("mjCTIMER_MESH_INERTIA", mjCTIMER_MESH_INERTIA) + .value("mjCTIMER_MESH_BVH", mjCTIMER_MESH_BVH) + .value("mjCTIMER_MESH_OCTREE", mjCTIMER_MESH_OCTREE) + .value("mjNCTIMER", mjNCTIMER); enum_("mjtCamLight") .value("mjCAMLIGHT_FIXED", mjCAMLIGHT_FIXED) .value("mjCAMLIGHT_TRACK", mjCAMLIGHT_TRACK) @@ -12371,6 +12387,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .property("nupdate", &MjSolverStat::nupdate, &MjSolverStat::set_nupdate, reference()); emscripten::class_("MjSpec") .constructor() + .property("timer", &MjSpec::timer) .property("comment", &MjSpec::comment, &MjSpec::set_comment, reference()) .property("compiler", &MjSpec::compiler, reference()) .property("element", &MjSpec::element, reference()) diff --git a/wasm/codegen/generators/constants.py b/wasm/codegen/generators/constants.py index 9c4832b2..c5a84f8b 100644 --- a/wasm/codegen/generators/constants.py +++ b/wasm/codegen/generators/constants.py @@ -176,6 +176,7 @@ _SKIPPED_GETTERS_AND_SETTERS: tuple[str, ...] = ( "mjs_getDouble", "mjs_getPluginAttributes", "mjs_getString", + "mjs_getTimer", "mjs_getUserValue", "mjs_setBuffer", "mjs_setDouble", diff --git a/wasm/codegen/generators/structs.py b/wasm/codegen/generators/structs.py index 24dd291f..0bb03066 100644 --- a/wasm/codegen/generators/structs.py +++ b/wasm/codegen/generators/structs.py @@ -381,6 +381,9 @@ def build_struct_header( builder.line(f"{s}* get() const;") builder.line(f"void set({s}* ptr);") + if w == "MjSpec": + builder.line("emscripten::val timer() const;") + # field declarations for field in wrapped_fields: if field.declaration and field not in member_inits: @@ -598,6 +601,7 @@ def _build_struct_bindings( #undef X_ACCESSOR""".lstrip()) elif w == "MjSpec": builder.line(".constructor()") + builder.line('.property("timer", &MjSpec::timer)') elif w == "MjvScene": builder.line(".constructor()") builder.line(".constructor<>()") diff --git a/wasm/codegen/templates/bindings.cc b/wasm/codegen/templates/bindings.cc index 05694fb6..e4255e15 100644 --- a/wasm/codegen/templates/bindings.cc +++ b/wasm/codegen/templates/bindings.cc @@ -715,6 +715,10 @@ MjSpec::~MjSpec() { mjSpec *MjSpec::get() const { return ptr_; } void MjSpec::set(mjSpec *ptr) { ptr_ = ptr; } +emscripten::val MjSpec::timer() const { + return emscripten::val(emscripten::typed_memory_view(9, mjs_getTimer(ptr_))); +} + std::unique_ptr mj_loadXML_wrapper_1(std::string filename) { char error[1000]; mjModel *model = mj_loadXML(filename.c_str(), nullptr, error, sizeof(error));