瀏覽代碼

HPCC-17312 Introduce a new scope iterator implementation

Signed-off-by: Gavin Halliday <gavin.halliday@lexisnexis.com>
Gavin Halliday 8 年之前
父節點
當前提交
fd9c41771b

+ 4 - 2
common/workunit/CMakeLists.txt

@@ -25,13 +25,15 @@
 project( workunit ) 
 
 set (    SRCS 
-         workunit.cpp 
+         workunit.cpp
+         wuattr.cpp
          wujobq.cpp
          package.cpp
          workflow.cpp
          referencedfilelist.cpp
                  
-         workunit.hpp 
+         workunit.hpp
+         wuattr.hpp
          wuerror.hpp
          wujobq.hpp
          package.h

文件差異過大導致無法顯示
+ 1197 - 80
common/workunit/workunit.cpp


+ 52 - 4
common/workunit/workunit.hpp

@@ -35,6 +35,7 @@
 #include "jstats.h"
 #include "jutil.hpp"
 #include "jprop.hpp"
+#include "wuattr.hpp"
 
 #define LEGACY_GLOBAL_SCOPE "workunit"
 #define GLOBAL_SCOPE ""
@@ -956,21 +957,31 @@ interface IConstWUAppValueIterator : extends IScmIterator
  */
 
 
-interface IConstWUStatistic : extends IInterface
+interface IConstWUScope : extends IInterface
+{
+    virtual IStringVal & getScope(IStringVal & str) const = 0;          // what scope is the statistic gathered over? e.g., workunit, wfid:n, graphn, graphn:m
+    virtual StatisticScopeType getScopeType() const = 0;
+};
+
+interface IConstStatistic : extends IInterface
 {
     virtual IStringVal & getDescription(IStringVal & str, bool createDefault) const = 0;    // Description of the statistic suitable for displaying to the user
     virtual IStringVal & getCreator(IStringVal & str) const = 0;        // what component gathered the statistic e.g., myroxie/eclserver_12/mythor:100
-    virtual IStringVal & getScope(IStringVal & str) const = 0;          // what scope is the statistic gathered over? e.g., workunit, wfid:n, graphn, graphn:m
     virtual IStringVal & getFormattedValue(IStringVal & str) const = 0; // The formatted value for display
     virtual StatisticMeasure getMeasure() const = 0;
     virtual StatisticKind getKind() const = 0;
     virtual StatisticCreatorType getCreatorType() const = 0;
-    virtual StatisticScopeType getScopeType() const = 0;
     virtual unsigned __int64 getValue() const = 0;
     virtual unsigned __int64 getCount() const = 0;
     virtual unsigned __int64 getMax() const = 0;
+};
+
+interface IConstWUStatistic : extends IConstStatistic
+{
+    virtual const char * queryScope() const = 0;          // what scope is the statistic gathered over? e.g., workunit, wfid:n, graphn, graphn:m
+    virtual StatisticScopeType getScopeType() const = 0;
     virtual unsigned __int64 getTimestamp() const = 0;  // time the statistic was created
-    virtual bool matches(const IStatisticsFilter * filter) const = 0;
+    virtual bool matches(const IStatisticsFilter * filter) const = 0; // This is an implementation detail, and shouldn't really be exported.
 };
 
 interface IConstWUStatisticIterator : extends IScmIterator
@@ -978,6 +989,42 @@ interface IConstWUStatisticIterator : extends IScmIterator
     virtual IConstWUStatistic & query() = 0;
 };
 
+/*
+ * An interface that is provided as a callback to a scope iterator to report the when iterating scopes
+ */
+interface IWuScopeVisitor
+{
+    virtual void noteStatistic(StatisticKind kind, unsigned __int64 value, IConstWUStatistic & extra) = 0;
+    virtual void noteAttribute(WuAttr attr, const char * value) = 0;
+    virtual void noteHint(const char * kind, const char * value) = 0;
+};
+
+/*
+ * Interface for an iterator that walks through the different logical elements (scopes) within a workunit
+ */
+interface IConstWUScopeIterator : extends IScmIterator
+{
+    //These return values are invalid after a call to next() or another call to the same function
+    virtual const char * queryScope() const = 0;
+    virtual StatisticScopeType getScopeType() const = 0;
+
+    //Provide information about all stats, attributes and hints
+    //MORE: should allow a mask to indicate which information is reported
+    virtual void playProperties(IWuScopeVisitor & visitor) = 0;
+
+    //Return true if the stat is present, if found and update the value - queryStat() wrapper is generally easier to use.
+    virtual bool getStat(StatisticKind kind, unsigned __int64 & value) const = 0;
+    virtual const char * queryAttribute(WuAttr attr) const = 0; // Multiple values can be processed via the playStatistics() function
+    virtual const char * queryHint(const char * kind) const = 0;
+
+    inline unsigned __int64 queryStat(StatisticKind kind, unsigned __int64 defaultValue = 0) const
+    {
+        unsigned __int64 value = defaultValue;
+        getStat(kind, value);
+        return value;
+    }
+};
+
 //! IWorkUnit
 //! Provides high level access to WorkUnit "header" data.
 interface IWorkUnit;
@@ -1058,6 +1105,7 @@ interface IConstWorkUnit : extends IConstWorkUnitInfo
     virtual IConstWUWebServicesInfo * getWebServicesInfo() const = 0;
     virtual IConstWUStatisticIterator & getStatistics(const IStatisticsFilter * filter) const = 0; // filter must currently stay alive while the iterator does.
     virtual IConstWUStatistic * getStatistic(const char * creator, const char * scope, StatisticKind kind) const = 0;
+    virtual IConstWUScopeIterator & getScopeIterator(const IStatisticsFilter * filter) const = 0; // filter must currently stay alive while the iterator does.
     virtual IConstWUResult * getVariableByName(const char * name) const = 0;
     virtual IConstWUResultIterator & getVariables() const = 0;
     virtual bool isPausing() const = 0;

+ 16 - 15
common/workunit/workunit.ipp

@@ -46,23 +46,23 @@ class WORKUNIT_API CLocalWUStatistic : implements IConstWUStatistic, public CInt
     Owned<IPropertyTree> p;
 public:
     IMPLEMENT_IINTERFACE;
+
     CLocalWUStatistic(IPropertyTree *p);
 
-    virtual IStringVal & getCreator(IStringVal & str) const;
-    virtual IStringVal & getDescription(IStringVal & str, bool createDefault) const;
-    virtual IStringVal & getFormattedValue(IStringVal & str) const;
-    virtual IStringVal & getType(IStringVal & str) const;
-    virtual IStringVal & getScope(IStringVal & str) const;
-    virtual StatisticMeasure getMeasure() const;
-    virtual StatisticCreatorType getCreatorType() const;
-    virtual StatisticScopeType getScopeType() const;
-    virtual StatisticKind getKind() const;
-    virtual unsigned __int64 getValue() const;
-    virtual unsigned __int64 getCount() const;
-    virtual unsigned __int64 getMax() const;
-    virtual unsigned __int64 getTimestamp() const;
-
-    virtual bool matches(const IStatisticsFilter * filter) const;
+    virtual IStringVal & getCreator(IStringVal & str) const override;
+    virtual IStringVal & getDescription(IStringVal & str, bool createDefault) const override;
+    virtual IStringVal & getFormattedValue(IStringVal & str) const override;
+    virtual const char * queryScope() const override;
+    virtual StatisticMeasure getMeasure() const override;
+    virtual StatisticCreatorType getCreatorType() const override;
+    virtual StatisticScopeType getScopeType() const override;
+    virtual StatisticKind getKind() const override;
+    virtual unsigned __int64 getValue() const override;
+    virtual unsigned __int64 getCount() const override;
+    virtual unsigned __int64 getMax() const override;
+    virtual unsigned __int64 getTimestamp() const override;
+
+    virtual bool matches(const IStatisticsFilter * filter) const override;
 };
 
 //==========================================================================================
@@ -303,6 +303,7 @@ public:
     virtual IConstWUResultIterator & getTemporaries() const;
     virtual IConstWUStatisticIterator & getStatistics(const IStatisticsFilter * filter) const;
     virtual IConstWUStatistic * getStatistic(const char * creator, const char * scope, StatisticKind kind) const;
+    virtual IConstWUScopeIterator & getScopeIterator(const IStatisticsFilter * filter) const override;
     virtual IConstWUWebServicesInfo * getWebServicesInfo() const;
     virtual IStringVal & getXmlParams(IStringVal & params, bool hidePasswords) const;
     virtual const IPropertyTree *getXmlParams() const;

+ 139 - 0
common/workunit/wuattr.cpp

@@ -0,0 +1,139 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#include "wuattr.hpp"
+#include "jptree.hpp"
+
+struct WuAttrInfo
+{
+public:
+    WuAttr kind;
+    StatisticMeasure measure;
+    const char * name;
+    const char * graphPath;
+    const char * childPath;
+    const char * dft;
+};
+
+#define CHILDPATH(x) "att[@name='" x "']/@value"
+#define ATTR(kind, measure, path)           { WA ## kind, measure, #kind, path, nullptr, nullptr }
+#define CHILD(kind, measure, path)    { WA ## kind, measure, #kind, CHILDPATH(path), path, nullptr }
+#define CHILD_D(kind, measure, path, dft)    { WA ## kind, measure, #kind, CHILDPATH(path), path, dft }
+
+
+const static WuAttrInfo attrInfo[] = {
+    { WANone, SMeasureNone, "none", nullptr, nullptr, nullptr },
+    CHILD(Kind, SMeasureEnum, "_kind"),
+    ATTR(Source, SMeasureText, "@source"),
+    ATTR(Target, SMeasureText, "@target"),
+    CHILD_D(SourceIndex, SMeasureText, "_sourceIndex", "0"),
+    CHILD_D(TargetIndex, SMeasureText, "_targetIndex", "0"),
+    ATTR(Label, SMeasureText, "@label"),
+    CHILD(IsDependency, SMeasureBool, "_dependsOn"),
+    CHILD(IsChildGraph, SMeasureBool, "_childGraph"),
+    CHILD(Definition, SMeasureText, "definition"),
+    CHILD(EclName, SMeasureText, "name"),
+    { WAMax, SMeasureNone, nullptr, nullptr, nullptr, nullptr }
+};
+
+
+MODULE_INIT(INIT_PRIORITY_STANDARD)
+{
+    static_assert(_elements_in(attrInfo) >= (WAMax-WANone)+1, "Elements missing from attrInfo[]");
+    static_assert(_elements_in(attrInfo) <= (WAMax-WANone)+1, "Extra elements in attrInfo[]");
+    for (unsigned i=0; i < _elements_in(attrInfo); i++)
+    {
+        assertex(attrInfo[i].kind == WANone + i);
+    }
+    return true;
+}
+
+
+const char * queryWuAttributeName(WuAttr kind)
+{
+    if ((kind >= WANone) && (kind < WAMax))
+        return attrInfo[kind-WANone].name;
+    return nullptr;
+}
+
+WuAttr queryWuAttribute(const char * kind)
+{
+    //MORE: This needs to use a hash table
+    for (unsigned i=WANone; i < WAMax; i++)
+    {
+        if (strieq(kind, attrInfo[i-WANone].name))
+            return (WuAttr)i;
+    }
+    return WANone;
+}
+
+extern WORKUNIT_API const char * queryAttributeValue(IPropertyTree & src, WuAttr kind)
+{
+    if ((kind <= WANone) || (kind >= WAMax))
+        return nullptr;
+
+    const WuAttrInfo & info = attrInfo[kind-WANone];
+    const char * path = info.graphPath;
+    const char * value = src.queryProp(path);
+    if (!value && info.dft)
+        value = info.dft;
+    return value;
+}
+
+extern WORKUNIT_API WuAttr queryGraphAttrToWuAttr(const char * name)
+{
+    //MORE: Create a hash table to implement this mapping efficiently
+    for(unsigned i=1; i < WAMax-WANone; i++)
+    {
+        const WuAttrInfo & info = attrInfo[i];
+        const char * path = info.graphPath;
+        if (path[0] == '@' && strieq(name, path+1))
+            return (WuAttr)(i+WANone);
+    }
+    return WANone;
+}
+
+extern WORKUNIT_API WuAttr queryGraphChildAttToWuAttr(const char * name)
+{
+    //MORE: Create a hash table to implement this mapping efficiently
+    for(unsigned i=1; i < WAMax-WANone; i++)
+    {
+        const WuAttrInfo & info = attrInfo[i];
+        const char * childPath = info.childPath;
+        if (childPath && strieq(name, childPath))
+            return (WuAttr)(i+WANone);
+    }
+    return WANone;
+}
+
+
+static IPropertyTree * addGraphAttribute(IPropertyTree * node, const char * name)
+{
+    IPropertyTree * att = createPTree();
+    att->setProp("@name", name);
+    return node->addPropTree("att", att);
+}
+
+extern WORKUNIT_API void setAttributeValue(IPropertyTree & tgt, WuAttr kind, const char * value)
+{
+    const WuAttrInfo & info = attrInfo[kind-WANone];
+    const char * path = info.graphPath;
+    if (path[0] == '@')
+        tgt.setProp(path, value);
+    else
+        addGraphAttribute(&tgt, info.childPath)->setProp("@value", value);
+}

+ 55 - 0
common/workunit/wuattr.hpp

@@ -0,0 +1,55 @@
+/*##############################################################################
+
+    HPCC SYSTEMS software Copyright (C) 2012 HPCC Systems®.
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+############################################################################## */
+
+#ifndef WUATTR_HPP
+#define WUATTR_HPP
+
+#include "jlib.hpp"
+#include "jstatcodes.h"
+
+#ifdef WORKUNIT_EXPORTS
+    #define WORKUNIT_API DECL_EXPORT
+#else
+    #define WORKUNIT_API DECL_IMPORT
+#endif
+
+//The wuattribute values start from a high value - so that they do not overlap with StXXX
+enum WuAttr
+{
+    WANone = 0x80000000,
+    WAKind,
+    WASource,
+    WATarget,
+    WASourceIndex,
+    WATargetIndex,
+    WALabel,
+    WAIsDependency,
+    WAIsChildGraph,
+    WADefinition,
+    WAEclName,
+    WAMax
+};
+
+extern WORKUNIT_API const char * queryWuAttributeName(WuAttr kind);
+extern WORKUNIT_API WuAttr queryWuAttribute(const char * kind);
+extern WORKUNIT_API const char * queryAttributeValue(IPropertyTree & src, WuAttr kind);
+extern WORKUNIT_API WuAttr queryGraphAttrToWuAttr(const char * name);
+extern WORKUNIT_API WuAttr queryGraphChildAttToWuAttr(const char * name);
+
+extern WORKUNIT_API void setAttributeValue(IPropertyTree & tgt, WuAttr kind, const char * value);
+
+#endif

+ 129 - 6
dali/daliadmin/daliadmin.cpp

@@ -2556,7 +2556,6 @@ static void dumpStats(IConstWorkUnit * workunit, const StatisticsFilter & filter
         IConstWUStatistic & cur = stats->query();
         StringBuffer xml;
         SCMStringBuffer curCreator;
-        SCMStringBuffer curScope;
         SCMStringBuffer curDescription;
         SCMStringBuffer curFormattedValue;
 
@@ -2568,8 +2567,8 @@ static void dumpStats(IConstWorkUnit * workunit, const StatisticsFilter & filter
         unsigned __int64 count = cur.getCount();
         unsigned __int64 max = cur.getMax();
         unsigned __int64 ts = cur.getTimestamp();
+        const char * curScope = cur.queryScope();
         cur.getCreator(curCreator);
-        cur.getScope(curScope);
         cur.getDescription(curDescription, false);
         cur.getFormattedValue(curFormattedValue);
 
@@ -2586,8 +2585,8 @@ static void dumpStats(IConstWorkUnit * workunit, const StatisticsFilter & filter
             if (curScopeType != SSTnone)
                 xml.append(queryScopeTypeName(curScopeType));
             xml.append(",");
-            if (curScope.length())
-                xml.append(curScope.str());
+            if (!isEmptyString(curScope))
+                xml.append(curScope);
             xml.append(",");
             if (curMeasure != SMeasureNone)
                 xml.append(queryMeasureName(curMeasure));
@@ -2620,8 +2619,8 @@ static void dumpStats(IConstWorkUnit * workunit, const StatisticsFilter & filter
                 xml.append("<creator>").append(curCreator.str()).append("</creator>");
             if (curScopeType != SSTnone)
                 xml.append("<stype>").append(queryScopeTypeName(curScopeType)).append("</stype>");
-            if (curScope.length())
-                xml.append("<scope>").append(curScope.str()).append("</scope>");
+            if (!isEmptyString(curScope))
+                xml.append("<scope>").append(curScope).append("</scope>");
             if (curMeasure != SMeasureNone)
                 xml.append("<unit>").append(queryMeasureName(curMeasure)).append("</unit>");
             if (curKind != StKindNone)
@@ -2680,6 +2679,117 @@ static void dumpStats(const char *wuid, const char * creatorTypeText, const char
     }
 }
 
+
+/* Callback used to output the different scope properties as xml */
+class ScopeDumper : public IWuScopeVisitor
+{
+public:
+    virtual void noteStatistic(StatisticKind kind, unsigned __int64 value, IConstWUStatistic & cur) override
+    {
+        StringBuffer xml;
+        SCMStringBuffer curCreator;
+        SCMStringBuffer curDescription;
+        SCMStringBuffer curFormattedValue;
+
+        StatisticCreatorType curCreatorType = cur.getCreatorType();
+        StatisticScopeType curScopeType = cur.getScopeType();
+        StatisticMeasure curMeasure = cur.getMeasure();
+        unsigned __int64 count = cur.getCount();
+        unsigned __int64 max = cur.getMax();
+        unsigned __int64 ts = cur.getTimestamp();
+        const char * curScope = cur.queryScope();
+        cur.getCreator(curCreator);
+        cur.getDescription(curDescription, false);
+        cur.getFormattedValue(curFormattedValue);
+
+        if (kind != StKindNone)
+            xml.append(" kind='").append(queryStatisticName(kind)).append("'");
+        xml.append(" value='").append(value).append("'");
+        xml.append(" formatted='").append(curFormattedValue).append("'");
+        if (curMeasure != SMeasureNone)
+            xml.append(" unit='").append(queryMeasureName(curMeasure)).append("'");
+        if (curCreatorType != SCTnone)
+            xml.append(" ctype='").append(queryCreatorTypeName(curCreatorType)).append("'");
+        if (curCreator.length())
+            xml.append(" creator='").append(curCreator.str()).append("'");
+        if (count != 1)
+            xml.append(" count='").append(count).append("'");
+        if (max)
+            xml.append(" max='").append(value).append("'");
+        if (ts)
+        {
+            xml.append(" ts='");
+            formatStatistic(xml, ts, SMeasureTimestampUs);
+            xml.append("'");
+        }
+        if (curDescription.length())
+            xml.append(" desc='").append(curDescription.str()).append("'");
+        printf(" <attr%s/>\n", xml.str());
+    }
+    virtual void noteAttribute(WuAttr attr, const char * value)
+    {
+        StringBuffer xml;
+        xml.appendf("<attr kind='%s' value='%s'/>", queryWuAttributeName(attr), value);
+        printf(" %s\n", xml.str());
+    }
+    virtual void noteHint(const char * kind, const char * value)
+    {
+        StringBuffer xml;
+        xml.appendf("<attr kind='hint:%s' value='%s'/>", kind, value);
+        printf(" %s\n", xml.str());
+    }
+};
+
+static void dumpWorkunitAttr(IConstWorkUnit * workunit, const StatisticsFilter & filter)
+{
+    ScopeDumper dumper;
+
+    printf("<Workunit wuid=\"%s\">\n", workunit->queryWuid());
+
+    Owned<IConstWUScopeIterator> iter = &workunit->getScopeIterator(&filter);
+    ForEach(*iter)
+    {
+        printf("<scope scope='%s' type='%s'>\n", iter->queryScope(), queryScopeTypeName(iter->getScopeType()));
+        iter->playProperties(dumper);
+        printf("</scope>\n");
+    }
+
+    printf("</Workunit>\n");
+}
+
+static void dumpWorkunitAttr(const char *wuid, const char * creatorTypeText, const char * creator, const char * scopeTypeText, const char * scope, const char * kindText, const char * userFilter)
+{
+    StatisticsFilter filter(checkDash(creatorTypeText), checkDash(creator), checkDash(scopeTypeText), checkDash(scope), NULL, checkDash(kindText));
+    if (userFilter)
+        filter.setFilter(userFilter);
+
+    Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
+    const char * star = strchr(wuid, '*');
+    if (star)
+    {
+        WUSortField filters[2];
+        MemoryBuffer filterbuf;
+        filters[0] = WUSFwildwuid;
+        filterbuf.append(wuid);
+        filters[1] = WUSFterm;
+        Owned<IConstWorkUnitIterator> iter = factory->getWorkUnitsSorted((WUSortField) (WUSFwuid), filters, filterbuf.bufferBase(), 0, INT_MAX, NULL, NULL);
+
+        ForEach(*iter)
+        {
+            Owned<IConstWorkUnit> workunit = factory->openWorkUnit(iter->query().queryWuid());
+            if (workunit)
+                dumpWorkunitAttr(workunit, filter);
+        }
+    }
+    else
+    {
+        Owned<IConstWorkUnit> workunit = factory->openWorkUnit(wuid);
+        if (!workunit)
+            return;
+        dumpWorkunitAttr(workunit, filter);
+    }
+}
+
 static void wuidCompress(const char *match, const char *type, bool compress)
 {
     if (0 != stricmp("graph", type))
@@ -3552,6 +3662,19 @@ int main(int argc, char* argv[])
                         }
                         migrateFiles(srcGroup, dstGroup, filemask, options);
                     }
+                    else if (stricmp(cmd, "wuattr") == 0) {
+                        CHECKPARAMS(1, 6);
+                        if ((params.ordinality() >= 3) && (strchr(params.item(2), '[')))
+                        {
+                            dumpWorkunitAttr(params.item(1), "-", "-", "-", "-", "-", params.item(2));
+                        }
+                        else
+                        {
+                            while (params.ordinality() < 7)
+                                params.append("*");
+                            dumpWorkunitAttr(params.item(1), params.item(2), params.item(3), params.item(4), params.item(5), params.item(6), nullptr);
+                        }
+                    }
                     else
                         ERRLOG("Unknown command %s",cmd);
                 }

+ 2 - 3
ecl/eclcc/eclcc.cpp

@@ -1954,14 +1954,13 @@ void EclCompileInstance::logStats(bool logTimings)
     if (logTimings)
     {
         Owned<IConstWUStatisticIterator> stats = &wu->getStatistics(nullptr);
-        SCMStringBuffer scope;
         ForEach(*stats)
         {
             IConstWUStatistic & cur = stats->query();
-            cur.getScope(scope);
+            const char * scope = cur.queryScope();
             OwnedPTree tree = createPTree("stat", ipt_fast);
             tree->setProp("@kind", queryStatisticName(cur.getKind()));
-            tree->setProp("@scope", scope.str());
+            tree->setProp("@scope", scope);
             tree->setPropInt("@scopeType", (unsigned)cur.getScopeType());
             tree->setPropInt64("@value", cur.getValue());
             tree->setPropInt64("@max", cur.getMax());

+ 10 - 11
esp/services/ws_workunits/ws_workunitsHelpers.cpp

@@ -390,12 +390,12 @@ void WsWuInfo::doGetTimers(IArrayOf<IEspECLTimer>& timers)
         ForEach(*it)
         {
             IConstWUStatistic & cur = it->query();
-            SCMStringBuffer name, scope;
+            SCMStringBuffer name;
             cur.getDescription(name, true);
-            cur.getScope(scope);
+            const char * scope = cur.queryScope();
 
             bool isThorTiming = false;//Should it be renamed as isClusterTiming?
-            if ((cur.getCreatorType() == SCTsummary) && (cur.getKind() == StTimeElapsed) && isGlobalScope(scope.str()))
+            if ((cur.getCreatorType() == SCTsummary) && (cur.getKind() == StTimeElapsed) && isGlobalScope(scope))
             {
                 SCMStringBuffer creator;
                 cur.getCreator(creator);
@@ -412,7 +412,7 @@ void WsWuInfo::doGetTimers(IArrayOf<IEspECLTimer>& timers)
                 totalThorTimerCount += cur.getCount();
             }
             else
-                addTimerToList(name, scope.str(), cur, timers);
+                addTimerToList(name, scope, cur, timers);
         }
     }
 
@@ -826,13 +826,12 @@ void WsWuInfo::getGraphTimingData(IArrayOf<IConstECLTimingData> &timingData)
     ForEach(*times)
     {
         IConstWUStatistic & cur = times->query();
-        SCMStringBuffer scope;
-        cur.getScope(scope);
+        const char * scope = cur.queryScope();
 
         StringAttr graphName;
         unsigned graphNum;
         unsigned subGraphId;
-        if (parseGraphScope(scope.str(), graphName, graphNum, subGraphId))
+        if (parseGraphScope(scope, graphName, graphNum, subGraphId))
         {
             unsigned time = (unsigned)nanoToMilli(cur.getValue());
 
@@ -1629,7 +1628,7 @@ void WsWuInfo::getStats(StatisticsFilter& filter, bool createDescriptions, IArra
     {
         IConstWUStatistic & cur = stats->query();
         StringBuffer xmlBuf, tsValue;
-        SCMStringBuffer curCreator, curScope, curDescription, curFormattedValue;
+        SCMStringBuffer curCreator, curDescription, curFormattedValue;
 
         StatisticCreatorType curCreatorType = cur.getCreatorType();
         StatisticScopeType curScopeType = cur.getScopeType();
@@ -1639,8 +1638,8 @@ void WsWuInfo::getStats(StatisticsFilter& filter, bool createDescriptions, IArra
         unsigned __int64 count = cur.getCount();
         unsigned __int64 max = cur.getMax();
         unsigned __int64 ts = cur.getTimestamp();
+        const char * curScope = cur.queryScope();
         cur.getCreator(curCreator);
-        cur.getScope(curScope);
         cur.getDescription(curDescription, createDescriptions);
         cur.getFormattedValue(curFormattedValue);
 
@@ -1654,8 +1653,8 @@ void WsWuInfo::getStats(StatisticsFilter& filter, bool createDescriptions, IArra
             wuStatistic->setCreator(curCreator.str());
         if (curScopeType != SSTnone)
             wuStatistic->setScopeType(queryScopeTypeName(curScopeType));
-        if (curScope.length())
-            wuStatistic->setScope(curScope.str());
+        if (!isEmpty(curScope))
+            wuStatistic->setScope(curScope);
         if (curMeasure != SMeasureNone)
             wuStatistic->setMeasure(queryMeasureName(curMeasure));
         if (curKind != StKindNone)

+ 4 - 7
plugins/workunitservices/workunitservices.cpp

@@ -539,9 +539,7 @@ WORKUNITSERVICES_API void wsWorkunitTimeStamps(ICodeContext *ctx, size32_t & __l
         {
             IConstWUStatistic & cur = stats->query();
 
-            SCMStringBuffer scope;
-            cur.getScope(scope);
-
+            const char * curScope = cur.queryScope();
             StatisticKind kind = cur.getKind();
             const char * kindName = queryStatisticName(kind);
             assertex(kindName && memicmp(kindName, "when", 4) == 0);
@@ -555,7 +553,7 @@ WORKUNITSERVICES_API void wsWorkunitTimeStamps(ICodeContext *ctx, size32_t & __l
             const char * at = strchr(creator.str(), '@');
             const char * instance = at ? at + 1 : creator.str();
 
-            fixedAppend(mb, 32, scope.str());
+            fixedAppend(mb, 32, curScope);
             fixedAppend(mb, 16, kindName); // id
             fixedAppend(mb, 20, formattedTime);            // time
             fixedAppend(mb, 16, instance);                 // item correct here
@@ -687,7 +685,7 @@ public:
         StatisticCreatorType creatorType = cur.getCreatorType();
         cur.getCreator(creator);
         StatisticScopeType scopeType = cur.getScopeType();
-        cur.getScope(scope);
+        const char * scope = cur.queryScope();
         cur.getDescription(description, true);
         StatisticMeasure measure = cur.getMeasure();
         StatisticKind kind = cur.getKind();
@@ -699,7 +697,7 @@ public:
         varAppend(mb, queryCreatorTypeName(creatorType));
         varAppend(mb, creator.str());
         varAppend(mb, queryScopeTypeName(scopeType));
-        varAppend(mb, scope.str());
+        varAppend(mb, scope);
         varAppend(mb, queryStatisticName(kind));
         varAppend(mb, description.str());
         varAppend(mb, queryMeasureName(measure));
@@ -724,7 +722,6 @@ protected:
     Linked<IEngineRowAllocator> resultAllocator;
     Linked<IConstWUStatisticIterator> iter;
     SCMStringBuffer creator;
-    SCMStringBuffer scope;
     SCMStringBuffer description;
 };
 

+ 36 - 16
system/jlib/jptree.cpp

@@ -140,6 +140,25 @@ static int comparePropTrees(IInterface * const *ll, IInterface * const *rr)
     return stricmp(l->queryName(), r->queryName());
 };
 
+class CPTArrayIterator : public ArrayIIteratorOf<IArrayOf<IPropertyTree>, IPropertyTree, IPropertyTreeIterator>
+{
+    IArrayOf<IPropertyTree> elems;
+public:
+    CPTArrayIterator(IPropertyTreeIterator &iter, TreeCompareFunc compare) : ArrayIIteratorOf<IArrayOf<IPropertyTree>, IPropertyTree, IPropertyTreeIterator>(elems)
+    {
+        ForEach(iter)
+            elems.append(iter.get());
+        elems.sort(compare);
+    }
+};
+IPropertyTreeIterator * createSortedIterator(IPropertyTreeIterator & iter)
+{
+    return new CPTArrayIterator(iter, comparePropTrees);
+}
+IPropertyTreeIterator * createSortedIterator(IPropertyTreeIterator & iter, TreeCompareFunc compare)
+{
+    return new CPTArrayIterator(iter, compare);
+}
 //////////////////
 
 unsigned ChildMap::getHashFromElement(const void *e) const
@@ -181,23 +200,10 @@ IPropertyTreeIterator *ChildMap::getIterator(bool sort)
         virtual bool isValid() override { return hiter->isValid(); }
         virtual IPropertyTree & query() override { return hiter->query(); }
     };
-    class CPTArrayIterator : public ArrayIIteratorOf<IArrayOf<IPropertyTree>, IPropertyTree, IPropertyTreeIterator>
-    {
-        IArrayOf<IPropertyTree> elems;
-    public:
-        CPTArrayIterator(IPropertyTreeIterator &iter) : ArrayIIteratorOf<IArrayOf<IPropertyTree>, IPropertyTree, IPropertyTreeIterator>(elems)
-        {
-            ForEach(iter)
-                elems.append(iter.get());
-            elems.sort(comparePropTrees);
-        }
-    };
-    IPropertyTreeIterator *baseIter = new CPTHashIterator(*this);
+    Owned<IPropertyTreeIterator> baseIter = new CPTHashIterator(*this);
     if (!sort)
-        return baseIter;
-    IPropertyTreeIterator *it = new CPTArrayIterator(*baseIter);
-    baseIter->Release();
-    return it;
+        return baseIter.getClear();
+    return createSortedIterator(*baseIter);
 }
 
 ///////////
@@ -5478,6 +5484,20 @@ void toXML(const IPropertyTree *tree, IIOStream &out, unsigned indent, unsigned
     _toXML(tree, out, indent, flags);
 }
 
+void printXML(const IPropertyTree *tree, unsigned indent, unsigned flags)
+{
+    StringBuffer xml;
+    toXML(tree, xml, indent, flags);
+    printf("%s", xml.str());
+}
+
+void dbglogXML(const IPropertyTree *tree, unsigned indent, unsigned flags)
+{
+    StringBuffer xml;
+    toXML(tree, xml, indent, flags);
+    DBGLOG("%s", xml.str());
+}
+
 void saveXML(const char *filename, const IPropertyTree *tree, unsigned indent, unsigned flags)
 {
     OwnedIFile ifile = createIFile(filename);

+ 5 - 0
system/jlib/jptree.hpp

@@ -230,6 +230,9 @@ jlib_decl IPropertyTree *createPTreeFromHttpPath(const char *nameWithAttrs, IPro
 jlib_decl IPropertyTree *createPTreeFromHttpParameters(const char *nameWithAttrs, IProperties *parameters, bool skipLeadingDotParameters, bool nestedRoot, ipt_flags flags=ipt_none);
 jlib_decl bool checkParseUrlPathNodeValue(const char *s, StringBuffer &name, StringAttr &value);
 
+typedef int (*TreeCompareFunc)(IInterface * const *ll, IInterface * const *rr);
+jlib_decl IPropertyTreeIterator * createSortedIterator(IPropertyTreeIterator & iter, TreeCompareFunc compare);
+
 
 #define XML_SortTags 0x01
 #define XML_Embed    0x02
@@ -248,6 +251,8 @@ jlib_decl void saveXML(const char *filename, const IPropertyTree *tree, unsigned
 jlib_decl void saveXML(IFile &ifile, const IPropertyTree *tree, unsigned indent = 0, unsigned=XML_Format);
 jlib_decl void saveXML(IFileIO &ifileio, const IPropertyTree *tree, unsigned indent = 0, unsigned flags=XML_Format);
 jlib_decl void saveXML(IIOStream &stream, const IPropertyTree *tree, unsigned indent = 0, unsigned flags=XML_Format);
+jlib_decl void printXML(const IPropertyTree *tree, unsigned indent = 0, unsigned flags=XML_Format);
+jlib_decl void dbglogXML(const IPropertyTree *tree, unsigned indent = 0, unsigned flags=XML_Format);
 
 #define JSON_SortTags 0x01
 #define JSON_Format   0x02

+ 6 - 0
system/jlib/jstatcodes.h

@@ -71,6 +71,7 @@ enum StatisticScopeType
     SSTfunction,                        // a function call
     SSTworkflow,
     SSTchildgraph,
+    SSTunknown,
     SSTmax
 };
 
@@ -88,6 +89,9 @@ enum StatisticMeasure
     SMeasurePercent,                    // actually stored as parts per million, displayed as a percentage
     SMeasureIPV4,
     SMeasureCycle,
+    SMeasureEnum,                       // A value from an enumeration
+    SMeasureText,                       // A textual value (from a graph attribute rather than a statistic)
+    SMeasureBool,                       // A boolean
     SMeasureMax,
 };
 
@@ -217,6 +221,8 @@ enum StatisticKind
     StStdDevX                           = 0xa0000,  // standard deviation in the value of X
     StNextModifier                      = 0xb0000,
 
+    //NOTE: Do not use 0x80000000 since wu attributes use those values, and they should not overlap
 };
+constexpr StatisticKind operator |(StatisticKind l, StatisticKind r) { return (StatisticKind)((unsigned)l | (unsigned)r); }
 
 #endif

+ 236 - 40
system/jlib/jstats.cpp

@@ -65,9 +65,9 @@ void setStatisticsComponentName(StatisticCreatorType processType, const char * p
 //--------------------------------------------------------------------------------------------------------------------
 
 // Textual forms of the different enumerations, first items are for none and all.
-static const char * const measureNames[] = { "", "all", "ns", "ts", "cnt", "sz", "cpu", "skw", "node", "ppm", "ip", "cy", NULL };
-static const char * const creatorTypeNames[]= { "", "all", "unknown", "hthor", "roxie", "roxie:s", "thor", "thor:m", "thor:s", "eclcc", "esp", "summary", NULL };
-static const char * const scopeTypeNames[] = { "", "all", "global", "graph", "subgraph", "activity", "allocator", "section", "compile", "dfu", "edge", "function", "workflow", "child", nullptr };
+static constexpr const char * const measureNames[] = { "", "all", "ns", "ts", "cnt", "sz", "cpu", "skw", "node", "ppm", "ip", "cy", "en", "txt", "bool", NULL };
+static constexpr const char * const creatorTypeNames[]= { "", "all", "unknown", "hthor", "roxie", "roxie:s", "thor", "thor:m", "thor:s", "eclcc", "esp", "summary", NULL };
+static constexpr const char * const scopeTypeNames[] = { "", "all", "global", "graph", "subgraph", "activity", "allocator", "section", "compile", "dfu", "edge", "function", "workflow", "child", "unknown", nullptr };
 
 static unsigned matchString(const char * const * names, const char * search)
 {
@@ -91,6 +91,62 @@ static unsigned matchString(const char * const * names, const char * search)
 
 //--------------------------------------------------------------------------------------------------------------------
 
+static const StatisticScopeType scoreOrder[] = {
+    SSTedge,
+    SSTactivity,
+    SSTnone,
+    SSTall,
+    SSTglobal,
+    SSTgraph,
+    SSTsubgraph,
+    SSTallocator,
+    SSTsection,
+    SSTcompilestage,
+    SSTdfuworkunit,
+    SSTfunction,
+    SSTworkflow,
+    SSTchildgraph,
+    SSTunknown
+};
+static int scopePriority[SSTmax];
+
+MODULE_INIT(INIT_PRIORITY_STANDARD)
+{
+    static_assert(_elements_in(scoreOrder) == SSTmax, "Elements missing from scoreOrder[]");
+    for (unsigned i=0; i < _elements_in(scoreOrder); i++)
+        scopePriority[scoreOrder[i]] = i;
+    return true;
+}
+
+
+extern jlib_decl int compareScopeName(const char * left, const char * right)
+{
+    StatsScopeId leftId;
+    StatsScopeId rightId;
+    for(;;)
+    {
+        leftId.extractScopeText(left, &left);
+        rightId.extractScopeText(right, &right);
+        int result = leftId.compare(rightId);
+        if (result != 0)
+            return result;
+        left = strchr(left, ':');
+        right = strchr(right, ':');
+        if (!left || !right)
+        {
+            if (left)
+                return +2;
+            if (right)
+                return -2;
+            return 0;
+        }
+        left++;
+        right++;
+    }
+}
+
+//--------------------------------------------------------------------------------------------------------------------
+
 extern jlib_decl unsigned __int64 getTimeStampNowValue()
 {
 #ifdef _WIN32
@@ -350,6 +406,9 @@ void formatStatistic(StringBuffer & out, unsigned __int64 value, StatisticMeasur
     case SMeasureCycle:
         out.append(value);
         break;
+    case SMeasureBool:
+        out.append(boolToStr(value != 0));
+        break;
     default:
         out.append(value).append('?');
     }
@@ -395,6 +454,9 @@ const char * queryMeasurePrefix(StatisticMeasure measure)
     case SMeasurePercent:       return "Per";
     case SMeasureIPV4:          return "Ip";
     case SMeasureCycle:         return "Cycle";
+    case SMeasureEnum:          return "";
+    case SMeasureText:          return "";
+    case SMeasureBool:          return "Is";
     default:
         return "Unknown";
     }
@@ -443,6 +505,9 @@ StatsMergeAction queryMergeMode(StatisticMeasure measure)
     case SMeasurePercent:       return StatsMergeReplace;
     case SMeasureIPV4:          return StatsMergeKeepNonZero;
     case SMeasureCycle:         return StatsMergeSum;
+    case SMeasureEnum:          return StatsMergeKeepNonZero;
+    case SMeasureText:          return StatsMergeKeepNonZero;
+    case SMeasureBool:          return StatsMergeKeepNonZero;
     default:
 #ifdef _DEBUG
         throwUnexpected();
@@ -1019,6 +1084,8 @@ StringBuffer & StatsScopeId::getScopeText(StringBuffer & out) const
         return out.append(WorkflowScopePrefix).append(id);
     case SSTchildgraph:
         return out.append(ChildGraphScopePrefix).append(id);
+    case SSTunknown:
+        return out.append(name);
     default:
 #ifdef _DEBUG
         throwUnexpected();
@@ -1032,12 +1099,32 @@ unsigned StatsScopeId::getHash() const
     switch (scopeType)
     {
     case SSTfunction:
+    case SSTunknown:
         return hashc((const byte *)name.get(), strlen(name), (unsigned)scopeType);
     default:
         return hashc((const byte *)&id, sizeof(id), (unsigned)scopeType);
     }
 }
 
+int StatsScopeId::compare(const StatsScopeId & other) const
+{
+    if (scopeType != other.scopeType)
+        return scopePriority[scopeType] - scopePriority[other.scopeType];
+    if (id != other.id)
+        return (int)(id - other.id);
+    if (extra != other.extra)
+        return (int)(extra - other.extra);
+    if (name && other.name)
+        return strcmp(name, other.name);
+    if (name)
+        return +1;
+    if (other.name)
+        return -1;
+    return 0;
+}
+
+
+
 bool StatsScopeId::matches(const StatsScopeId & other) const
 {
     return (scopeType == other.scopeType) && (id == other.id) && (extra == other.extra) && strsame(name, other.name);
@@ -1114,37 +1201,107 @@ void StatsScopeId::setId(StatisticScopeType _scopeType, unsigned _id, unsigned _
     extra = _extra;
 }
 
-bool StatsScopeId::setScopeText(const char * text)
+bool StatsScopeId::setScopeText(const char * text, char * * next)
 {
-    if (MATCHES_CONST_PREFIX(text, ActivityScopePrefix))
-        setActivityId(atoi(text + CONST_STRLEN(ActivityScopePrefix)));
-    else if (MATCHES_CONST_PREFIX(text, GraphScopePrefix))
-        setId(SSTgraph, atoi(text + CONST_STRLEN(GraphScopePrefix)));
-    else if (MATCHES_CONST_PREFIX(text, SubGraphScopePrefix))
-        setSubgraphId(atoi(text + CONST_STRLEN(SubGraphScopePrefix)));
-    else if (MATCHES_CONST_PREFIX(text, EdgeScopePrefix))
+    switch (*text)
     {
-        const char * underscore = strchr(text, '_');
-        if (!underscore)
-            return false;
-        setEdgeId(atoi(text + CONST_STRLEN(EdgeScopePrefix)), atoi(underscore+1));
-    }
-    else if (MATCHES_CONST_PREFIX(text, FunctionScopePrefix))
-        setFunctionId(text+CONST_STRLEN(FunctionScopePrefix));
-    else if (MATCHES_CONST_PREFIX(text, WorkflowScopePrefix))
-        setWorkflowId(atoi(text+CONST_STRLEN(WorkflowScopePrefix)));
-    else if (MATCHES_CONST_PREFIX(text, ChildGraphScopePrefix))
-        setChildGraphId(atoi(text+CONST_STRLEN(ChildGraphScopePrefix)));
-    else
-        return false;
+    case ActivityScopePrefix[0]:
+        if (MATCHES_CONST_PREFIX(text, ActivityScopePrefix))
+        {
+            if (isdigit(text[CONST_STRLEN(ActivityScopePrefix)]))
+            {
+                unsigned id = strtoul(text + CONST_STRLEN(ActivityScopePrefix), next, 10);
+                setActivityId(id);
+                return true;
+            }
+        }
+        break;
+    case GraphScopePrefix[0]:
+        if (MATCHES_CONST_PREFIX(text, GraphScopePrefix))
+        {
+            if (isdigit(text[CONST_STRLEN(GraphScopePrefix)]))
+            {
+                unsigned id = strtoul(text + CONST_STRLEN(GraphScopePrefix), next, 10);
+                setId(SSTgraph, id);
+                return true;
+            }
+        }
+        break;
+    case SubGraphScopePrefix[0]:
+        if (MATCHES_CONST_PREFIX(text, SubGraphScopePrefix))
+        {
+            if (isdigit(text[CONST_STRLEN(SubGraphScopePrefix)]))
+            {
+                unsigned id = strtoul(text + CONST_STRLEN(SubGraphScopePrefix), next, 10);
+                setSubgraphId(id);
+                return true;
+            }
+        }
+        break;
+    case EdgeScopePrefix[0]:
+        if (MATCHES_CONST_PREFIX(text, EdgeScopePrefix))
+        {
+            const char * underscore = strchr(text, '_');
+            if (!underscore || !isdigit(underscore[1]))
+                return false;
+            unsigned id1 = atoi(text + CONST_STRLEN(EdgeScopePrefix));
+            unsigned id2 = strtoul(underscore+1, next, 10);
+            setEdgeId(id1, id2);
+            return true;
+        }
+        break;
+    case FunctionScopePrefix[0]:
+        if (MATCHES_CONST_PREFIX(text, FunctionScopePrefix))
+        {
+            setFunctionId(text+CONST_STRLEN(FunctionScopePrefix));
+            return true;
+        }
+        break;
+    case WorkflowScopePrefix[0]:
+        if (MATCHES_CONST_PREFIX(text, WorkflowScopePrefix))
+        {
+            setWorkflowId(atoi(text+CONST_STRLEN(WorkflowScopePrefix)));
+            return true;
+        }
+        break;
+    case ChildGraphScopePrefix[0]:
+        if (MATCHES_CONST_PREFIX(text, ChildGraphScopePrefix))
+        {
+            setChildGraphId(atoi(text+CONST_STRLEN(ChildGraphScopePrefix)));
+            return true;
+        }
+        break;
+    }
+    return false;
+}
 
-    return true;
+void StatsScopeId::extractScopeText(const char * text, const char * * next)
+{
+    if (!setScopeText(text, (char * *)next))
+    {
+        scopeType = SSTunknown;
+        const char * end = strchr(text, ':');
+        if (end)
+        {
+            name.set(text, end-text);
+            if (next)
+                *next = end;
+        }
+        else
+        {
+            name.set(text);
+            if (next)
+                *next = text + strlen(text);
+        }
+    }
 }
 
+
 void StatsScopeId::setActivityId(unsigned _id)
 {
     setId(SSTactivity, _id);
 }
+
 void StatsScopeId::setEdgeId(unsigned _id, unsigned _output)
 {
     setId(SSTedge, _id, _output);
@@ -1195,6 +1352,20 @@ public:
 };
 typedef IArrayOf<CStatisticCollection> CollectionArray;
 
+static int compareCollection(IInterface * const * pl, IInterface * const *pr);
+
+class SortedCollectionIterator : public ArrayIIteratorOf<IArrayOf<IStatisticCollection>, IStatisticCollection, IStatisticCollectionIterator>
+{
+    IArrayOf<IStatisticCollection> elems;
+public:
+    SortedCollectionIterator(IStatisticCollectionIterator &iter) : ArrayIIteratorOf<IArrayOf<IStatisticCollection>, IStatisticCollection, IStatisticCollectionIterator>(elems)
+    {
+        ForEach(iter)
+            elems.append(iter.get());
+        elems.sort(compareCollection);
+    }
+};
+
 class CStatisticCollection : public CInterfaceOf<IStatisticCollection>
 {
     friend class CollectionHashTable;
@@ -1231,21 +1402,21 @@ public:
     StringBuffer &toXML(StringBuffer &out) const;
 
 //interface IStatisticCollection:
-    virtual StatisticScopeType queryScopeType() const
+    virtual StatisticScopeType queryScopeType() const override
     {
         return id.queryScopeType();
     }
-    virtual unsigned __int64 queryWhenCreated() const
+    virtual unsigned __int64 queryWhenCreated() const override
     {
         if (parent)
             return parent->queryWhenCreated();
         return 0;
     }
-    virtual StringBuffer & getScope(StringBuffer & str) const
+    virtual StringBuffer & getScope(StringBuffer & str) const override
     {
         return id.getScopeText(str);
     }
-    virtual StringBuffer & getFullScope(StringBuffer & str) const
+    virtual StringBuffer & getFullScope(StringBuffer & str) const override
     {
         if (parent)
         {
@@ -1255,7 +1426,7 @@ public:
         id.getScopeText(str);
         return str;
     }
-    virtual unsigned __int64 queryStatistic(StatisticKind kind) const
+    virtual unsigned __int64 queryStatistic(StatisticKind kind) const override
     {
         ForEachItemIn(i, stats)
         {
@@ -1265,23 +1436,39 @@ public:
         }
         return 0;
     }
-    virtual unsigned getNumStatistics() const
+    virtual bool getStatistic(StatisticKind kind, unsigned __int64 & value) const override
+    {
+        ForEachItemIn(i, stats)
+        {
+            const Statistic & cur = stats.item(i);
+            if (cur.kind == kind)
+            {
+                value = cur.value;
+                return true;
+            }
+        }
+        return false;
+    }
+    virtual unsigned getNumStatistics() const override
     {
         return stats.ordinality();
     }
-    virtual void getStatistic(StatisticKind & kind, unsigned __int64 & value, unsigned idx) const
+    virtual void getStatistic(StatisticKind & kind, unsigned __int64 & value, unsigned idx) const override
     {
         const Statistic & cur = stats.item(idx);
         kind = cur.kind;
         value = cur.value;
     }
-    virtual IStatisticCollectionIterator & getScopes(const char * filter)
+    virtual IStatisticCollectionIterator & getScopes(const char * filter, bool sorted) override
     {
         assertex(!filter);
-        return * new SuperHashIIteratorOf<IStatisticCollection, IStatisticCollectionIterator, false>(children);
+        Owned<IStatisticCollectionIterator> hashIter = new SuperHashIIteratorOf<IStatisticCollection, IStatisticCollectionIterator, false>(children);
+        if (!sorted)
+            return *hashIter.getClear();
+        return * new SortedCollectionIterator(*hashIter);
     }
 
-    virtual void getMinMaxScope(IStringVal & minValue, IStringVal & maxValue, StatisticScopeType searchScopeType) const
+    virtual void getMinMaxScope(IStringVal & minValue, IStringVal & maxValue, StatisticScopeType searchScopeType) const override
     {
         if (id.queryScopeType() == searchScopeType)
         {
@@ -1300,7 +1487,7 @@ public:
             iter.query().getMinMaxScope(minValue, maxValue, searchScopeType);
     }
 
-    virtual void getMinMaxActivity(unsigned & minValue, unsigned & maxValue) const
+    virtual void getMinMaxActivity(unsigned & minValue, unsigned & maxValue) const override
     {
         unsigned activityId = id.queryActivity();
         if (activityId)
@@ -1379,6 +1566,8 @@ public:
             iter.query().serialize(out);
     }
 
+    inline const StatsScopeId & queryScopeId() const { return id; }
+
 private:
     StatsScopeId id;
     CStatisticCollection * parent;
@@ -1405,6 +1594,13 @@ StringBuffer &CStatisticCollection::toXML(StringBuffer &out) const
     return out;
 }
 
+static int compareCollection(IInterface * const * pl, IInterface * const *pr)
+{
+    CStatisticCollection * l = static_cast<CStatisticCollection *>(static_cast<IStatisticCollection *>(*pl));
+    CStatisticCollection * r = static_cast<CStatisticCollection *>(static_cast<IStatisticCollection *>(*pr));
+    return l->queryScopeId().compare(r->queryScopeId());
+}
+
 //---------------------------------------------------------------------------------------------------------------------
 
 void CollectionHashTable::onAdd(void *et)
@@ -2577,7 +2773,7 @@ void StatisticsFilter::setKind(const char * _kind)
     {
         const char * prefix = queryMeasurePrefix((StatisticMeasure)i1);
         size_t len = strlen(prefix);
-        if (strnicmp(_kind, prefix, len) == 0)
+        if (len && strnicmp(_kind, prefix, len) == 0)
         {
             setMeasure((StatisticMeasure)i1);
             //Treat When* and When as filters on times.
@@ -2674,9 +2870,9 @@ static void checkDistributedKind(StatisticKind kind)
 
 void verifyStatisticFunctions()
 {
-    assertex(_elements_in(measureNames) == SMeasureMax+1 && !measureNames[SMeasureMax]);
-    assertex(_elements_in(creatorTypeNames) == SCTmax+1 && !creatorTypeNames[SCTmax]);
-    assertex(_elements_in(scopeTypeNames) == SSTmax+1 && !scopeTypeNames[SSTmax]);
+    static_assert(_elements_in(measureNames) == SMeasureMax+1 && !measureNames[SMeasureMax], "measureNames needs updating");
+    static_assert(_elements_in(creatorTypeNames) == SCTmax+1 && !creatorTypeNames[SCTmax], "creatorTypeNames needs updating");
+    static_assert(_elements_in(scopeTypeNames) == SSTmax+1 && !scopeTypeNames[SSTmax], "scopeTypeNames needs updating");
 
     //Check the various functions return values for all possible values.
     for (unsigned i1=SMeasureAll; i1 < SMeasureMax; i1++)

+ 11 - 2
system/jlib/jstats.h

@@ -54,7 +54,8 @@ public:
     void deserialize(MemoryBuffer & in, unsigned version);
     void serialize(MemoryBuffer & out) const;
 
-    bool setScopeText(const char * text);
+    void extractScopeText(const char * text, const char * * next);
+    bool setScopeText(const char * text, char * * next = nullptr);
     void setId(StatisticScopeType _scopeType, unsigned _id, unsigned _extra = 0);
     void setActivityId(unsigned _id);
     void setEdgeId(unsigned _id, unsigned _output);
@@ -63,6 +64,8 @@ public:
     void setWorkflowId(unsigned _id);
     void setChildGraphId(unsigned _id);
 
+    int compare(const StatsScopeId & other) const;
+
     bool operator == (const StatsScopeId & other) const { return matches(other); }
 
 protected:
@@ -82,8 +85,9 @@ public:
     virtual StringBuffer & getScope(StringBuffer & str) const = 0;
     virtual unsigned __int64 queryStatistic(StatisticKind kind) const = 0;
     virtual unsigned getNumStatistics() const = 0;
+    virtual bool getStatistic(StatisticKind kind, unsigned __int64 & value) const = 0;
     virtual void getStatistic(StatisticKind & kind, unsigned __int64 & value, unsigned idx) const = 0;
-    virtual IStatisticCollectionIterator & getScopes(const char * filter) = 0;
+    virtual IStatisticCollectionIterator & getScopes(const char * filter, bool sorted) = 0;
     virtual void getMinMaxScope(IStringVal & minValue, IStringVal & maxValue, StatisticScopeType searchScopeType) const = 0;
     virtual void getMinMaxActivity(unsigned & minValue, unsigned & maxValue) const = 0;
     virtual void serialize(MemoryBuffer & out) const = 0;
@@ -599,6 +603,11 @@ extern jlib_decl void verifyStatisticFunctions();
 extern jlib_decl void formatTimeCollatable(StringBuffer & out, unsigned __int64 value, bool nano);
 extern jlib_decl unsigned __int64 extractTimeCollatable(const char *s, bool nano);
 
+//Scopes need to be processed in a consistent order so they can be merged.
+//activities are in numeric order
+//edges must come before activities.
+extern jlib_decl int compareScopeName(const char * left, const char * right);
+
 //This interface is primarily here to reduce the dependency between the different components.
 interface IStatisticTarget
 {

+ 77 - 1
tools/wutool/wutool.cpp

@@ -235,10 +235,13 @@ int main(int argc, const char *argv[])
 #ifdef _USE_CPPUNIT
         if (action && (stricmp(action, "-selftest")==0))
         {
+            StringBuffer testName;
+            if (!globals->getProp("test", testName))
+                testName.set("WuTool");
             testSize = globals->getPropInt("testSize", 100);
             queryStderrLogMsgHandler()->setMessageFields(MSGFIELD_time | MSGFIELD_milliTime | MSGFIELD_prefix);
             CppUnit::TextUi::TestRunner runner;
-            CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry("WuTool");
+            CppUnit::TestFactoryRegistry &registry = CppUnit::TestFactoryRegistry::getRegistry(testName.str());
             runner.addTest( registry.makeTest() );
             ret = runner.run( "", false );
         }
@@ -1952,4 +1955,77 @@ StringArray WuTool::wuids;
 CPPUNIT_TEST_SUITE_REGISTRATION( WuTool );
 CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( WuTool, "WuTool" );
 
+class WuDetails : public CppUnit::TestFixture
+{
+    CPPUNIT_TEST_SUITE(WuDetails);
+        CPPUNIT_TEST(testWuDetails);
+    CPPUNIT_TEST_SUITE_END();
+protected:
+    class AttributeScopeVisitor : public IWuScopeVisitor
+    {
+    public:
+        virtual void noteStatistic(StatisticKind kind, unsigned __int64 value, IConstWUStatistic & cur) override
+        {
+            StringBuffer text;
+            text.append(value);
+            noteProperty(queryStatisticName(kind), text);
+        }
+        virtual void noteAttribute(WuAttr attr, const char * value)
+        {
+            noteProperty(queryWuAttributeName(attr), value);
+        }
+        virtual void noteHint(const char * kind, const char * value)
+        {
+            noteProperty(kind, value);
+        }
+        virtual void noteProperty(const char * kind, const char * value)
+        {
+            DBGLOG("  Attr %s=%s", kind, value);
+        }
+    };
+
+    void testWuDetails(IConstWorkUnit * wu)
+    {
+        const StatisticsFilter filter;
+        Owned<IConstWUScopeIterator> iter = &wu->getScopeIterator(&filter);
+        DBGLOG("%s %s", wu->queryWuid(), wu->queryClusterName());
+        AttributeScopeVisitor visitor;
+        StringBuffer prevScope;
+        ForEach(*iter)
+        {
+            const char * scope = iter->queryScope();
+            DBGLOG("Scope: %s %s", scope, queryScopeTypeName(iter->getScopeType()));
+            //Ensure the scopes are iterated in the correct order.
+            if (!prevScope.isEmpty())
+                ASSERT(compareScopeName(prevScope.str(), scope) < 0);
+            prevScope.set(scope);
+
+            iter->playProperties(visitor);
+        }
+    }
+
+    void testWuDetails()
+    {
+#if 0
+        WUSortField filterByJob[] = { WUSFjob, WUSFterm };
+        CCycleTimer timer;
+        Owned<IConstWorkUnitIterator> wus;
+        Owned<IWorkUnitFactory> factory = getWorkUnitFactory();
+        wus.setown(factory->getWorkUnitsSorted((WUSortField) (WUSFwuid | WUSFreverse), filterByJob, "sqfilt-multiPart(false)*\0", 0, 10000, NULL, NULL));
+        ForEach(*wus)
+        {
+            IConstWorkUnitInfo &wuinfo = wus->query();
+            Owned<IConstWorkUnit> wu = factory->openWorkUnit(wuinfo.queryWuid());
+            testWuDetails(wu);
+        }
+        wus.clear();
+
+        DBGLOG("wuDetails %u ms", timer.elapsedMs());
+#endif
+    }
+};
+
+CPPUNIT_TEST_SUITE_REGISTRATION( WuDetails );
+CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( WuDetails, "WuDetails" );
+
 #endif