From 60399db07ea655960ae6979afa3a6aed0a8045bf Mon Sep 17 00:00:00 2001 From: Larry Gritz Date: Sun, 16 Aug 2026 11:58:58 -0700 Subject: [PATCH] feat: Query ranked/aggregate group compile stats via getattribute Post-optimization group metrics active_layers, network_depth, texture_ops, and noise_ops were queryable per group, but the "top N" lists and min/max/median that getstats() prints were computed inline and discarded. Renderers had no way to get them short of scraping the text, and no way to enumerate groups to recompute them. Add system-level getattribute keys: stat:compiled_:top_count int # groups with value > 0 stat:compiled_:top_names string[n] # ranked, "" padded stat:compiled_:top_values int[n] # ranked, 0 padded stat:compiled_:min|max|median int # zero-valued included Caller's array size picks N; stat:rank_groups still governs only the printed report. Only optimized groups count, and querying never forces optimization. The gather/sort/aggregate logic moves into gather_group_stats(), which getstats() now also uses, so the report and the queries cannot drift. Keys dispatch before m_mutex is taken, since gather_group_stats() needs only m_all_shader_groups_mutex. oslexec.h gains documentation only, so there is no API or ABI change. Testing: testrender gains --print-group-stats, and compstats renders a three-group scene, which testshade cannot do with one group per process. That covers ranking order, the name-ascending tie-break, and exclusion of zero-valued groups from the ranked list while they still count toward min/max/median. The test's filter_re was also dropping the individual ranked entries from both sides of the diff, so the printed top-N was never actually compared; widened to include them. Assisted-by: Claude Code / Claude Opus 5 Signed-off-by: Larry Gritz --- src/include/OSL/oslexec.h | 27 +++++ src/liboslexec/oslexec_pvt.h | 16 +++ src/liboslexec/shadingsys.cpp | 194 ++++++++++++++++++++++---------- src/testrender/testrender.cpp | 33 ++++++ src/testshade/testshade.cpp | 48 +++++--- testsuite/compstats/mtl.osl | 10 ++ testsuite/compstats/ref/out.txt | 39 +++++++ testsuite/compstats/run.py | 24 +++- testsuite/compstats/scene.xml | 35 ++++++ 9 files changed, 350 insertions(+), 76 deletions(-) create mode 100644 testsuite/compstats/mtl.osl create mode 100644 testsuite/compstats/scene.xml diff --git a/src/include/OSL/oslexec.h b/src/include/OSL/oslexec.h index 89950d27d..b007d3d4d 100644 --- a/src/include/OSL/oslexec.h +++ b/src/include/OSL/oslexec.h @@ -406,6 +406,33 @@ class OSLEXECPUBLIC ShadingSystem { /// library build dependencies and their versions (for example, /// "OIIO-2.3.0,LLVM-10.0.0,OpenEXR-2.5.0"). /// + /// - `stat:compiled_:` : Post-optimization compile + /// statistics aggregated over *all* shader groups, where `` is + /// one of `active_layers`, `network_depth`, `texture_ops`, or + /// `noise_ops` (the same metrics available for a single group via + /// `getattribute(group, "stat:compiled_", ...)`), and + /// `` is one of: + /// + /// - `int top_count` : How many groups have a nonzero value for this + /// metric. This is the full count, not clipped by the + /// `stat:rank_groups` option (which affects only the printed report). + /// - `string top_names[n]` : Names of the highest-valued groups, best + /// first. Fills `min(n, top_count)` entries; any remainder is filled + /// with empty strings. A group that was never named yields an empty + /// string. + /// - `int top_values[n]` : The corresponding metric values, in the same + /// order as `top_names`; any remainder is filled with 0. + /// - `int min`, `int max`, `int median` : Distribution of the metric. + /// Unlike the ranked list, these include groups whose value is 0. + /// + /// Only groups that have already been optimized are considered; + /// querying these does not force optimization of any group. Ranking is + /// by value descending, ties broken by group name ascending. If no + /// group has been optimized yet, all of these succeed and report 0. + /// Each query re-gathers from the live groups, so `top_names` and + /// `top_values` may disagree if groups are created or destroyed + /// between the two calls. + /// bool getattribute(string_view name, TypeDesc type, void* val); /// Shortcut getattribute() for retrieving a single integer. diff --git a/src/liboslexec/oslexec_pvt.h b/src/liboslexec/oslexec_pvt.h index 204074d89..7b0a529e5 100644 --- a/src/liboslexec/oslexec_pvt.h +++ b/src/liboslexec/oslexec_pvt.h @@ -615,6 +615,22 @@ class ShadingSystemImpl { std::string getstats(int level = 1) const; + /// One metric's worth of post-optimization compile statistics, gathered + /// fresh from all currently live, optimized shader groups. + struct GroupStatSnapshot { + // (value, groupname) for the groups whose value is > 0, sorted by + // value descending, then by group name ascending. + std::vector> ranked; + int vmin = 0, vmax = 0, vmedian = 0; ///< Over ALL optimized groups + int ngroups = 0; ///< How many optimized groups were examined + }; + + /// Gather the named per-group compile stat -- one of "active_layers", + /// "network_depth", "texture_ops", "noise_ops" -- across every live, + /// optimized shader group. Returns false (leaving snap untouched) if the + /// metric name is not recognized. + bool gather_group_stats(string_view metric, GroupStatSnapshot& snap) const; + ErrorHandler& errhandler() const { return *m_err; } ShaderMaster::ref loadshader(string_view name); diff --git a/src/liboslexec/shadingsys.cpp b/src/liboslexec/shadingsys.cpp index 367d5897b..6c64c203f 100644 --- a/src/liboslexec/shadingsys.cpp +++ b/src/liboslexec/shadingsys.cpp @@ -1814,6 +1814,58 @@ ShadingSystemImpl::getattribute(string_view name, TypeDesc type, void* val) return true; \ } + // Ranked and aggregate post-optimization compile stats, across all live + // optimized groups: + // stat:compiled_:top_count int -- # groups with value>0 + // stat:compiled_:top_names string[] -- ranked group names + // stat:compiled_:top_values int[] -- ranked values + // stat:compiled_:min/max/median int -- incl. zero-valued + // These are handled before taking m_mutex: gather_group_stats() needs + // only m_all_shader_groups_mutex, and acquiring that while holding + // m_mutex would establish a new lock ordering. + if (Strutil::starts_with(name, "stat:compiled_")) { + string_view rest = name.substr(sizeof("stat:compiled_") - 1); + size_t colon = rest.find(':'); + GroupStatSnapshot snap; + if (colon != string_view::npos + && gather_group_stats(rest.substr(0, colon), snap)) { + string_view sub = rest.substr(colon + 1); + if (sub == "top_count" && type == TypeInt) { + *(int*)val = (int)snap.ranked.size(); + return true; + } + if (sub == "top_names" && type.basetype == TypeDesc::STRING) { + size_t n = std::min(type.numelements(), snap.ranked.size()); + for (size_t i = 0; i < n; ++i) + ((ustring*)val)[i] = snap.ranked[i].second; + for (size_t i = n; i < type.numelements(); ++i) + ((ustring*)val)[i] = ustring(); + return true; + } + if (sub == "top_values" && type.basetype == TypeDesc::INT) { + size_t n = std::min(type.numelements(), snap.ranked.size()); + for (size_t i = 0; i < n; ++i) + ((int*)val)[i] = snap.ranked[i].first; + for (size_t i = n; i < type.numelements(); ++i) + ((int*)val)[i] = 0; + return true; + } + if (sub == "min" && type == TypeInt) { + *(int*)val = snap.vmin; + return true; + } + if (sub == "max" && type == TypeInt) { + *(int*)val = snap.vmax; + return true; + } + if (sub == "median" && type == TypeInt) { + *(int*)val = snap.vmedian; + return true; + } + } + // Unrecognized metric or subkey: fall through to the ordinary lookup. + } + lock_guard guard(m_mutex); // Thread safety ATTR_DECODE_STRING("searchpath:shader", m_searchpath); @@ -2759,66 +2811,31 @@ ShadingSystemImpl::getstats(int level) const // Ranked shader groups by compile-time complexity metrics if (m_stat_groups_compiled > 0) { - // Collect a snapshot of all still-live compiled (optimized) groups. - std::vector groups; - { - spin_lock lock(m_all_shader_groups_mutex); - for (auto&& w : m_all_shader_groups) - if (ShaderGroupRef g = w.lock()) - if (g->optimized()) - groups.push_back(g); - } - using StatVal = std::pair; print(out, " Shader compilation stats, post-optimized:\n"); - auto emit_ranked_groups = - [&](string_view label, string_view unit, - std::function getter) { - if (groups.empty()) - return; - // Gather values from all compiled groups for aggregate stats. - std::vector vals; - vals.reserve(groups.size()); - for (auto&& g : groups) - vals.push_back(getter(*g)); - std::sort(vals.begin(), vals.end()); - int vmin = vals.front(); - int vmax = vals.back(); - int vmedian = vals[vals.size() / 2]; - print(out, " {}: min={} max={} median={}\n", label, vmin, - vmax, vmedian); - // Ranked list: exclude groups with value 0. - std::vector ranked; - for (auto&& g : groups) { - int v = getter(*g); - if (v > 0) - ranked.emplace_back(v, g->name()); - } - if (ranked.empty()) - return; - std::sort(ranked.begin(), ranked.end(), - [](const StatVal& a, const StatVal& b) { - return a.first != b.first ? a.first > b.first - : a.second < b.second; - }); - if ((int)ranked.size() > m_stat_rank_groups) - ranked.resize(m_stat_rank_groups); - print(out, " Top shader groups:\n"); - for (auto&& [v, name] : ranked) - print(out, " {:>6} {} \"{}\"\n", v, unit, - name.size() ? name.c_str() : ""); - }; - emit_ranked_groups("Active layers", "layers", [](const ShaderGroup& g) { - return g.stat_active_layers(); - }); - emit_ranked_groups("Network depth", "depth", [](const ShaderGroup& g) { - return g.stat_network_depth(); - }); - emit_ranked_groups("Texture ops", "ops", [](const ShaderGroup& g) { - return g.stat_texture_ops(); - }); - emit_ranked_groups("Noise ops", "ops", [](const ShaderGroup& g) { - return g.stat_noise_ops(); - }); + // Purely formatting -- gather_group_stats() does the gathering, + // sorting and aggregating, so this report and the "stat:compiled_*" + // getattribute queries can never disagree. + auto emit_ranked_groups = [&](string_view label, string_view unit, + string_view metric) { + GroupStatSnapshot snap; + if (!gather_group_stats(metric, snap) || snap.ngroups == 0) + return; + print(out, " {}: min={} max={} median={}\n", label, snap.vmin, + snap.vmax, snap.vmedian); + if (snap.ranked.empty()) + return; + int n = std::min((int)snap.ranked.size(), m_stat_rank_groups); + print(out, " Top shader groups:\n"); + for (int i = 0; i < n; ++i) { + auto&& [v, name] = snap.ranked[i]; + print(out, " {:>6} {} \"{}\"\n", v, unit, + name.size() ? name.c_str() : ""); + } + }; + emit_ranked_groups("Active layers", "layers", "active_layers"); + emit_ranked_groups("Network depth", "depth", "network_depth"); + emit_ranked_groups("Texture ops", "ops", "texture_ops"); + emit_ranked_groups("Noise ops", "ops", "noise_ops"); } return out.str(); @@ -2826,6 +2843,65 @@ ShadingSystemImpl::getstats(int level) const +bool +ShadingSystemImpl::gather_group_stats(string_view metric, + GroupStatSnapshot& snap) const +{ + // Captureless lambdas convert to plain function pointers. + int (*getter)(const ShaderGroup&) = nullptr; + if (metric == "active_layers") + getter = [](const ShaderGroup& g) { return g.stat_active_layers(); }; + else if (metric == "network_depth") + getter = [](const ShaderGroup& g) { return g.stat_network_depth(); }; + else if (metric == "texture_ops") + getter = [](const ShaderGroup& g) { return g.stat_texture_ops(); }; + else if (metric == "noise_ops") + getter = [](const ShaderGroup& g) { return g.stat_noise_ops(); }; + else + return false; + + // Collect a snapshot of all still-live compiled (optimized) groups. + // Note that unlike the per-group getattribute() queries, this never + // forces optimization of a group that isn't optimized yet. + std::vector groups; + { + spin_lock lock(m_all_shader_groups_mutex); + for (auto&& w : m_all_shader_groups) + if (ShaderGroupRef g = w.lock()) + if (g->optimized()) + groups.push_back(g); + } + snap.ngroups = (int)groups.size(); + if (groups.empty()) + return true; + + // Aggregates are over all compiled groups, including zero-valued ones. + std::vector vals; + vals.reserve(groups.size()); + for (auto&& g : groups) + vals.push_back(getter(*g)); + std::sort(vals.begin(), vals.end()); + snap.vmin = vals.front(); + snap.vmax = vals.back(); + snap.vmedian = vals[vals.size() / 2]; + + // Ranked list: exclude groups with value 0. + using StatVal = std::pair; + for (auto&& g : groups) { + int v = getter(*g); + if (v > 0) + snap.ranked.emplace_back(v, g->name()); + } + std::sort(snap.ranked.begin(), snap.ranked.end(), + [](const StatVal& a, const StatVal& b) { + return a.first != b.first ? a.first > b.first + : a.second < b.second; + }); + return true; +} + + + void ShadingSystemImpl::printstats() const { diff --git a/src/testrender/testrender.cpp b/src/testrender/testrender.cpp index 899e3fd2f..bcc04e4f7 100644 --- a/src/testrender/testrender.cpp +++ b/src/testrender/testrender.cpp @@ -41,6 +41,7 @@ static bool runstats = false; static bool saveptx = false; static bool warmup = false; static bool profile = false; +static bool print_group_stats = false; static bool O0 = false, O1 = false, O2 = false; static int llvm_opt = 1; // LLVM optimization level static bool debugnan = false; @@ -164,6 +165,8 @@ getargs(int argc, const char* argv[]) .hidden(); // DEPRECATED 1.7 ap.arg("--profile", &profile) .help("Print profile information"); + ap.arg("--print-group-stats", &print_group_stats) + .help("Print ranked/aggregate compile stats across all shader groups"); ap.arg("--saveptx", &saveptx) .help("Save the generated PTX (OptiX mode only)"); ap.arg("--warmup", &warmup) @@ -378,6 +381,36 @@ main(int argc, const char* argv[]) std::cout << ustring::getstats() << "\n"; } + if (print_group_stats) { + // Ranked and aggregate compile stats across every shader group the + // shading system knows about, via the system-level getattribute. + static const char* metrics[] = { "active_layers", "network_depth", + "texture_ops", "noise_ops" }; + for (const char* metric : metrics) { + std::string key = OSL::fmtformat("stat:compiled_{}", metric); + int count = 0, vmin = 0, vmax = 0, vmedian = 0; + shadingsys->getattribute(key + ":top_count", count); + shadingsys->getattribute(key + ":min", vmin); + shadingsys->getattribute(key + ":max", vmax); + shadingsys->getattribute(key + ":median", vmedian); + OSL::print("{}: min={} max={} median={} top_count={}\n", key, vmin, + vmax, vmedian, count); + if (count <= 0) + continue; + std::vector names(count); + std::vector values(count); + shadingsys->getattribute(key + ":top_names", + TypeDesc(TypeDesc::STRING, count), + names.data()); + shadingsys->getattribute(key + ":top_values", + TypeDesc(TypeDesc::INT, count), + values.data()); + for (int i = 0; i < count; ++i) + OSL::print("{}: top[{}]={} \"{}\"\n", key, i, values[i], + names[i]); + } + } + // We're done with the shading system now, destroy it rend->clear(); delete shadingsys; diff --git a/src/testshade/testshade.cpp b/src/testshade/testshade.cpp index 20836ffff..19388893e 100644 --- a/src/testshade/testshade.cpp +++ b/src/testshade/testshade.cpp @@ -2339,20 +2339,40 @@ test_shade(int argc, const char* argv[]) } if (print_group_stats && !batched) { - int active_layers = 0, network_depth = 0, texture_ops = 0, - noise_ops = 0; - shadingsys->getattribute(shadergroup.get(), - "stat:compiled_active_layers", active_layers); - shadingsys->getattribute(shadergroup.get(), - "stat:compiled_network_depth", network_depth); - shadingsys->getattribute(shadergroup.get(), "stat:compiled_texture_ops", - texture_ops); - shadingsys->getattribute(shadergroup.get(), "stat:compiled_noise_ops", - noise_ops); - OSL::print("stat:compiled_active_layers={}\n", active_layers); - OSL::print("stat:compiled_network_depth={}\n", network_depth); - OSL::print("stat:compiled_texture_ops={}\n", texture_ops); - OSL::print("stat:compiled_noise_ops={}\n", noise_ops); + static const char* metrics[] = { "active_layers", "network_depth", + "texture_ops", "noise_ops" }; + // Per-group values. + for (const char* metric : metrics) { + std::string key = OSL::fmtformat("stat:compiled_{}", metric); + int v = 0; + shadingsys->getattribute(shadergroup.get(), key, v); + OSL::print("{}={}\n", key, v); + } + // Ranked and aggregate values across all of the shading system's + // groups, via the system-level (no ShaderGroup*) getattribute. + for (const char* metric : metrics) { + std::string key = OSL::fmtformat("stat:compiled_{}", metric); + int count = 0, vmin = 0, vmax = 0, vmedian = 0; + shadingsys->getattribute(key + ":top_count", count); + shadingsys->getattribute(key + ":min", vmin); + shadingsys->getattribute(key + ":max", vmax); + shadingsys->getattribute(key + ":median", vmedian); + OSL::print("{}: min={} max={} median={} top_count={}\n", key, vmin, + vmax, vmedian, count); + if (count <= 0) + continue; + std::vector names(count); + std::vector values(count); + shadingsys->getattribute(key + ":top_names", + TypeDesc(TypeDesc::STRING, count), + names.data()); + shadingsys->getattribute(key + ":top_values", + TypeDesc(TypeDesc::INT, count), + values.data()); + for (int i = 0; i < count; ++i) + OSL::print("{}: top[{}]={} \"{}\"\n", key, i, values[i], + names[i]); + } } diff --git a/testsuite/compstats/mtl.osl b/testsuite/compstats/mtl.osl new file mode 100644 index 000000000..f6dba0ba8 --- /dev/null +++ b/testsuite/compstats/mtl.osl @@ -0,0 +1,10 @@ +// Copyright Contributors to the Open Shading Language project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/AcademySoftwareFoundation/OpenShadingLanguage + +// Terminal layer for the multi-group scene: consumes an upstream color so +// that the layers feeding it survive optimization. +surface mtl(color Cin = 0) +{ + Ci = Cin * emission(); +} diff --git a/testsuite/compstats/ref/out.txt b/testsuite/compstats/ref/out.txt index dee1c62fd..408897967 100644 --- a/testsuite/compstats/ref/out.txt +++ b/testsuite/compstats/ref/out.txt @@ -15,3 +15,42 @@ stat:compiled_active_layers=2 stat:compiled_network_depth=2 stat:compiled_texture_ops=1 stat:compiled_noise_ops=2 +stat:compiled_active_layers: min=2 max=2 median=2 top_count=1 +stat:compiled_active_layers: top[0]=2 "complex" +stat:compiled_network_depth: min=2 max=2 median=2 top_count=1 +stat:compiled_network_depth: top[0]=2 "complex" +stat:compiled_texture_ops: min=1 max=1 median=1 top_count=1 +stat:compiled_texture_ops: top[0]=1 "complex" +stat:compiled_noise_ops: min=2 max=2 median=2 top_count=1 +stat:compiled_noise_ops: top[0]=2 "complex" + Shader compilation stats, post-optimized: + Active layers: min=2 max=3 median=2 + Top shader groups: + 3 layers "heavy" + 2 layers "light" + 2 layers "mid" + Network depth: min=2 max=3 median=2 + Top shader groups: + 3 depth "heavy" + 2 depth "light" + 2 depth "mid" + Texture ops: min=0 max=1 median=0 + Top shader groups: + 1 ops "heavy" + Noise ops: min=0 max=2 median=2 + Top shader groups: + 2 ops "heavy" + 2 ops "mid" +stat:compiled_active_layers: min=2 max=3 median=2 top_count=3 +stat:compiled_active_layers: top[0]=3 "heavy" +stat:compiled_active_layers: top[1]=2 "light" +stat:compiled_active_layers: top[2]=2 "mid" +stat:compiled_network_depth: min=2 max=3 median=2 top_count=3 +stat:compiled_network_depth: top[0]=3 "heavy" +stat:compiled_network_depth: top[1]=2 "light" +stat:compiled_network_depth: top[2]=2 "mid" +stat:compiled_texture_ops: min=0 max=1 median=0 top_count=1 +stat:compiled_texture_ops: top[0]=1 "heavy" +stat:compiled_noise_ops: min=0 max=2 median=2 top_count=2 +stat:compiled_noise_ops: top[0]=2 "heavy" +stat:compiled_noise_ops: top[1]=2 "mid" diff --git a/testsuite/compstats/run.py b/testsuite/compstats/run.py index e9078d511..037d8b6ce 100644 --- a/testsuite/compstats/run.py +++ b/testsuite/compstats/run.py @@ -30,7 +30,25 @@ " -o Cout null" ) -# Filter to only the new per-group ranked stats lines and getattribute -# stat key output; everything else is machine- or build-specific. +# A single testshade run only ever builds one group, so use testrender for a +# scene with three groups of differing complexity. This exercises the parts +# of the ranking that one group cannot: ordering, the group-name-ascending +# tie-break, and exclusion of zero-valued groups from the ranked list while +# they still count toward min/max/median. +# +# heavy: layer_a -> layer_b -> mtl 3 layers, depth 3, 1 texture, 2 noise +# mid: layer_b -> mtl 2 layers, depth 2, 0 texture, 2 noise +# light: simple -> mtl 2 layers, depth 2, 0 texture, 0 noise +# --runstats prints getstats() while the groups are still alive (the +# statistics:level option instead reports at shading system teardown, by +# which time the renderer has released most of its groups). +command += testrender( + "-r 32 32 -aa 1 --runstats --print-group-stats scene.xml out.exr" +) + +# Filter to only the per-group ranked stats lines and getattribute stat key +# output; everything else is machine- or build-specific. The +# '\d+ (layers|depth|ops) "' alternative keeps the individual ranked entries +# under "Top shader groups:", so the printed ranking is really compared. # Note: runtest uses re.match() (anchored at line start), so prefix with .* -filter_re = r".*(Shader compilation stats|Active layers|Network depth|Texture ops|Noise ops|Top shader groups|stat:)" +filter_re = r".*(Shader compilation stats|Active layers|Network depth|Texture ops|Noise ops|Top shader groups|\d+ (layers|depth|ops) \"|stat:)" diff --git a/testsuite/compstats/scene.xml b/testsuite/compstats/scene.xml new file mode 100644 index 000000000..cd7718846 --- /dev/null +++ b/testsuite/compstats/scene.xml @@ -0,0 +1,35 @@ + + + + + + + shader layer_a la; + shader layer_b lb; + shader mtl m; + connect la.Cout lb.Cin; + connect lb.Cout m.Cin; + + + + + shader layer_b lb; + shader mtl m; + connect lb.Cout m.Cin; + + + + + shader simple s; + shader mtl m; + connect s.Cout m.Cin; + + +