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; + + +