Changeset 195376 in webkit


Ignore:
Timestamp:
Jan 20, 2016 1:51:00 PM (8 years ago)
Author:
sbarati@apple.com
Message:

Web Inspector: Hook the sampling profiler into the Timelines UI
https://bugs.webkit.org/show_bug.cgi?id=152766
<rdar://problem/24066360>

Reviewed by Joseph Pecoraro.

Source/JavaScriptCore:

This patch adds some necessary functions to SamplingProfiler::StackFrame
to allow it to give data to the Inspector for the timelines UI. i.e, the
sourceID of the executable of a stack frame.

This patch also swaps in the SamplingProfiler in place of the
LegacyProfiler inside InspectorScriptProfilerAgent. It adds
the necessary protocol data to allow the SamplingProfiler's
data to hook into the timelines UI.

  • debugger/Debugger.cpp:

(JSC::Debugger::setProfilingClient):
(JSC::Debugger::willEvaluateScript):
(JSC::Debugger::didEvaluateScript):
(JSC::Debugger::toggleBreakpoint):

  • debugger/Debugger.h:
  • debugger/ScriptProfilingScope.h:

(JSC::ScriptProfilingScope::ScriptProfilingScope):
(JSC::ScriptProfilingScope::~ScriptProfilingScope):

  • inspector/agents/InspectorScriptProfilerAgent.cpp:

(Inspector::InspectorScriptProfilerAgent::willDestroyFrontendAndBackend):
(Inspector::InspectorScriptProfilerAgent::startTracking):
(Inspector::InspectorScriptProfilerAgent::stopTracking):
(Inspector::InspectorScriptProfilerAgent::isAlreadyProfiling):
(Inspector::InspectorScriptProfilerAgent::willEvaluateScript):
(Inspector::InspectorScriptProfilerAgent::didEvaluateScript):
(Inspector::InspectorScriptProfilerAgent::addEvent):
(Inspector::buildSamples):
(Inspector::InspectorScriptProfilerAgent::trackingComplete):
(Inspector::buildAggregateCallInfoInspectorObject): Deleted.
(Inspector::buildInspectorObject): Deleted.
(Inspector::buildProfileInspectorObject): Deleted.

  • inspector/agents/InspectorScriptProfilerAgent.h:
  • inspector/protocol/ScriptProfiler.json:
  • jsc.cpp:

(functionSamplingProfilerStackTraces):

  • runtime/SamplingProfiler.cpp:

(JSC::SamplingProfiler::start):
(JSC::SamplingProfiler::stop):
(JSC::SamplingProfiler::clearData):
(JSC::SamplingProfiler::StackFrame::displayName):
(JSC::SamplingProfiler::StackFrame::displayNameForJSONTests):
(JSC::SamplingProfiler::StackFrame::startLine):
(JSC::SamplingProfiler::StackFrame::startColumn):
(JSC::SamplingProfiler::StackFrame::sourceID):
(JSC::SamplingProfiler::StackFrame::url):
(JSC::SamplingProfiler::stackTraces):
(JSC::SamplingProfiler::stackTracesAsJSON):
(JSC::displayName): Deleted.
(JSC::SamplingProfiler::stacktracesAsJSON): Deleted.

  • runtime/SamplingProfiler.h:

(JSC::SamplingProfiler::StackFrame::StackFrame):
(JSC::SamplingProfiler::getLock):
(JSC::SamplingProfiler::setTimingInterval):
(JSC::SamplingProfiler::totalTime):
(JSC::SamplingProfiler::setStopWatch):
(JSC::SamplingProfiler::stackTraces): Deleted.

  • tests/stress/sampling-profiler-anonymous-function.js:

(platformSupportsSamplingProfiler.baz):
(platformSupportsSamplingProfiler):

  • tests/stress/sampling-profiler-basic.js:

(platformSupportsSamplingProfiler.nothing):
(platformSupportsSamplingProfiler.top):

  • tests/stress/sampling-profiler/samplingProfiler.js:

(doesTreeHaveStackTrace):

Source/WebInspectorUI:

The main change in this patch is to swap in the SamplingProfiler
in place of the LegacyProfiler. To do this, we've created a data
structure called CallingContextTree which aggregates the SamplingProfiler's
data into an easy to manage tree. To see how the data structure works,
consider the following program:
`
function bar() { run code here for a long time. }
function baz() {
run code here for a long time. }
function foo() { bar(); baz(); }
foo();
`
From this program, we will create a tree like this:

(program)

|
|

foo
| |

/ \

/ \

bar baz

From this type of tree, we can easily create a CPUProfile payload
object. Because the Timelines UI knows how to interact with the
CPUProfile object and display it, we currently map the tree to this object
to make it trivially easy to display the SamplingProfiler's data. In the future,
we may want to find ways to work directly with the CallingContextTree instead
of mapping it into another object.

  • Localizations/en.lproj/localizedStrings.js:
  • UserInterface/Controllers/TimelineManager.js:
  • UserInterface/Main.html:
  • UserInterface/Models/CallingContextTree.js: Added.
  • UserInterface/Models/ScriptInstrument.js:
  • UserInterface/Protocol/ScriptProfilerObserver.js:
  • UserInterface/TestStub.html:
  • UserInterface/Views/ScriptTimelineView.js:

LayoutTests:

  • inspector/sampling-profiler: Added.
  • inspector/sampling-profiler/basic-expected.txt: Added.
  • inspector/sampling-profiler/basic.html: Added.
  • inspector/sampling-profiler/call-frame-with-dom-functions-expected.txt: Added.
  • inspector/sampling-profiler/call-frame-with-dom-functions.html: Added.
  • inspector/sampling-profiler/eval-source-url-expected.txt: Added.
  • inspector/sampling-profiler/eval-source-url.html: Added.
  • inspector/sampling-profiler/many-call-frames-expected.txt: Added.
  • inspector/sampling-profiler/many-call-frames.html: Added.
  • inspector/sampling-profiler/named-function-expression-expected.txt: Added.
  • inspector/sampling-profiler/named-function-expression.html: Added.
  • inspector/script-profiler/event-type-API-expected.txt:
  • inspector/script-profiler/event-type-API.html:
  • inspector/script-profiler/event-type-Microtask-expected.txt:
  • inspector/script-profiler/event-type-Microtask.html:
  • inspector/script-profiler/event-type-Other-expected.txt:
  • inspector/script-profiler/event-type-Other.html:
  • inspector/script-profiler/tracking-expected.txt:
  • inspector/script-profiler/tracking.html:
Location:
trunk
Files:
12 added
30 edited

Legend:

Unmodified
Added
Removed
  • trunk/LayoutTests/ChangeLog

    r195374 r195376  
     12016-01-20  Saam barati  <sbarati@apple.com>
     2
     3        Web Inspector: Hook the sampling profiler into the Timelines UI
     4        https://bugs.webkit.org/show_bug.cgi?id=152766
     5        <rdar://problem/24066360>
     6
     7        Reviewed by Joseph Pecoraro.
     8
     9        * inspector/sampling-profiler: Added.
     10        * inspector/sampling-profiler/basic-expected.txt: Added.
     11        * inspector/sampling-profiler/basic.html: Added.
     12        * inspector/sampling-profiler/call-frame-with-dom-functions-expected.txt: Added.
     13        * inspector/sampling-profiler/call-frame-with-dom-functions.html: Added.
     14        * inspector/sampling-profiler/eval-source-url-expected.txt: Added.
     15        * inspector/sampling-profiler/eval-source-url.html: Added.
     16        * inspector/sampling-profiler/many-call-frames-expected.txt: Added.
     17        * inspector/sampling-profiler/many-call-frames.html: Added.
     18        * inspector/sampling-profiler/named-function-expression-expected.txt: Added.
     19        * inspector/sampling-profiler/named-function-expression.html: Added.
     20        * inspector/script-profiler/event-type-API-expected.txt:
     21        * inspector/script-profiler/event-type-API.html:
     22        * inspector/script-profiler/event-type-Microtask-expected.txt:
     23        * inspector/script-profiler/event-type-Microtask.html:
     24        * inspector/script-profiler/event-type-Other-expected.txt:
     25        * inspector/script-profiler/event-type-Other.html:
     26        * inspector/script-profiler/tracking-expected.txt:
     27        * inspector/script-profiler/tracking.html:
     28
    1292016-01-20  Daniel Bates  <dabates@apple.com>
    230
  • trunk/LayoutTests/inspector/script-profiler/event-type-API-expected.txt

    r194242 r195376  
    99PASS: Event type should be API.
    1010ScriptProfiler.trackingComplete
    11 PASS: Profiles should exist when complete.
    12 PASS: Should be 1 profile for this session.
    1311
  • trunk/LayoutTests/inspector/script-profiler/event-type-API.html

    r194242 r195376  
    2828            InspectorProtocol.awaitEvent({event: "ScriptProfiler.trackingComplete"}).then((messageObject) => {
    2929                ProtocolTest.log("ScriptProfiler.trackingComplete");
    30                 ProtocolTest.expectThat(Array.isArray(messageObject.params.profiles), "Profiles should exist when complete.");
    31                 ProtocolTest.expectThat(messageObject.params.profiles.length === 1, "Should be 1 profile for this session.");
    3230                resolve();
    3331            });
    3432
    35             InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {profile: true});
     33            InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {});
    3634            ProtocolTest.evaluateInPage("triggerAPIScript();"); // This ultimately uses the JSEvaluateScript API on the Page's context.
    3735            InspectorProtocol.sendCommand("ScriptProfiler.stopTracking", {});
  • trunk/LayoutTests/inspector/script-profiler/event-type-Microtask-expected.txt

    r194242 r195376  
    99PASS: Event type should be Microtask.
    1010ScriptProfiler.trackingComplete
    11 PASS: Profiles should exist when complete.
    12 PASS: Should be 1 profile for this session.
    1311
  • trunk/LayoutTests/inspector/script-profiler/event-type-Microtask.html

    r194242 r195376  
    2929            InspectorProtocol.awaitEvent({event: "ScriptProfiler.trackingComplete"}).then((messageObject) => {
    3030                ProtocolTest.log("ScriptProfiler.trackingComplete");
    31                 ProtocolTest.expectThat(Array.isArray(messageObject.params.profiles), "Profiles should exist when complete.");
    32                 ProtocolTest.expectThat(messageObject.params.profiles.length === 1, "Should be 1 profile for this session.");
    3331                resolve();
    3432            });
    3533
    36             InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {profile: true});
     34            InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {});
    3735            ProtocolTest.evaluateInPage("triggerMicrotask()");
    3836            InspectorProtocol.sendCommand("ScriptProfiler.stopTracking", {});
  • trunk/LayoutTests/inspector/script-profiler/event-type-Other-expected.txt

    r194871 r195376  
    1515PASS: Event type should be Other.
    1616ScriptProfiler.trackingComplete
    17 PASS: Profiles should exist when complete.
    18 PASS: Should be 3 profiles for this session.
    1917
  • trunk/LayoutTests/inspector/script-profiler/event-type-Other.html

    r195147 r195376  
    4242            InspectorProtocol.awaitEvent({event: "ScriptProfiler.trackingComplete"}).then((messageObject) => {
    4343                ProtocolTest.log("ScriptProfiler.trackingComplete");
    44                 ProtocolTest.expectThat(Array.isArray(messageObject.params.profiles), "Profiles should exist when complete.");
    45                 ProtocolTest.expectThat(messageObject.params.profiles.length === 3, "Should be 3 profiles for this session.");
    4644                resolve();
    4745            });
    4846
    49             InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {profile: true});
     47            InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {});
    5048            ProtocolTest.evaluateInPage("triggerScriptEvaluation()");
    5149            ProtocolTest.evaluateInPage("triggerEventDispatchEvaluation()");
  • trunk/LayoutTests/inspector/script-profiler/tracking-expected.txt

    r194242 r195376  
    77PASS: Should have a timestamp when starting.
    88ScriptProfiler.trackingComplete
    9 PASS: Profiles should exist when complete.
    10 PASS: Should be no profiles for this session.
    119
  • trunk/LayoutTests/inspector/script-profiler/tracking.html

    r194242 r195376  
    1919            InspectorProtocol.awaitEvent({event: "ScriptProfiler.trackingComplete"}).then((messageObject) => {
    2020                ProtocolTest.log("ScriptProfiler.trackingComplete");
    21                 ProtocolTest.expectThat(Array.isArray(messageObject.params.profiles), "Profiles should exist when complete.");
    22                 ProtocolTest.expectThat(!messageObject.params.profiles.length, "Should be no profiles for this session.");
    2321                resolve();
    2422            });
    2523
    26             InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {profile: true});
     24            InspectorProtocol.sendCommand("ScriptProfiler.startTracking", {});
    2725            InspectorProtocol.sendCommand("ScriptProfiler.stopTracking", {});
    2826        }
  • trunk/Source/JavaScriptCore/ChangeLog

    r195375 r195376  
     12016-01-20  Saam barati  <sbarati@apple.com>
     2
     3        Web Inspector: Hook the sampling profiler into the Timelines UI
     4        https://bugs.webkit.org/show_bug.cgi?id=152766
     5        <rdar://problem/24066360>
     6
     7        Reviewed by Joseph Pecoraro.
     8
     9        This patch adds some necessary functions to SamplingProfiler::StackFrame
     10        to allow it to give data to the Inspector for the timelines UI. i.e, the
     11        sourceID of the executable of a stack frame.
     12
     13        This patch also swaps in the SamplingProfiler in place of the
     14        LegacyProfiler inside InspectorScriptProfilerAgent. It adds
     15        the necessary protocol data to allow the SamplingProfiler's
     16        data to hook into the timelines UI.
     17
     18        * debugger/Debugger.cpp:
     19        (JSC::Debugger::setProfilingClient):
     20        (JSC::Debugger::willEvaluateScript):
     21        (JSC::Debugger::didEvaluateScript):
     22        (JSC::Debugger::toggleBreakpoint):
     23        * debugger/Debugger.h:
     24        * debugger/ScriptProfilingScope.h:
     25        (JSC::ScriptProfilingScope::ScriptProfilingScope):
     26        (JSC::ScriptProfilingScope::~ScriptProfilingScope):
     27        * inspector/agents/InspectorScriptProfilerAgent.cpp:
     28        (Inspector::InspectorScriptProfilerAgent::willDestroyFrontendAndBackend):
     29        (Inspector::InspectorScriptProfilerAgent::startTracking):
     30        (Inspector::InspectorScriptProfilerAgent::stopTracking):
     31        (Inspector::InspectorScriptProfilerAgent::isAlreadyProfiling):
     32        (Inspector::InspectorScriptProfilerAgent::willEvaluateScript):
     33        (Inspector::InspectorScriptProfilerAgent::didEvaluateScript):
     34        (Inspector::InspectorScriptProfilerAgent::addEvent):
     35        (Inspector::buildSamples):
     36        (Inspector::InspectorScriptProfilerAgent::trackingComplete):
     37        (Inspector::buildAggregateCallInfoInspectorObject): Deleted.
     38        (Inspector::buildInspectorObject): Deleted.
     39        (Inspector::buildProfileInspectorObject): Deleted.
     40        * inspector/agents/InspectorScriptProfilerAgent.h:
     41        * inspector/protocol/ScriptProfiler.json:
     42        * jsc.cpp:
     43        (functionSamplingProfilerStackTraces):
     44        * runtime/SamplingProfiler.cpp:
     45        (JSC::SamplingProfiler::start):
     46        (JSC::SamplingProfiler::stop):
     47        (JSC::SamplingProfiler::clearData):
     48        (JSC::SamplingProfiler::StackFrame::displayName):
     49        (JSC::SamplingProfiler::StackFrame::displayNameForJSONTests):
     50        (JSC::SamplingProfiler::StackFrame::startLine):
     51        (JSC::SamplingProfiler::StackFrame::startColumn):
     52        (JSC::SamplingProfiler::StackFrame::sourceID):
     53        (JSC::SamplingProfiler::StackFrame::url):
     54        (JSC::SamplingProfiler::stackTraces):
     55        (JSC::SamplingProfiler::stackTracesAsJSON):
     56        (JSC::displayName): Deleted.
     57        (JSC::SamplingProfiler::stacktracesAsJSON): Deleted.
     58        * runtime/SamplingProfiler.h:
     59        (JSC::SamplingProfiler::StackFrame::StackFrame):
     60        (JSC::SamplingProfiler::getLock):
     61        (JSC::SamplingProfiler::setTimingInterval):
     62        (JSC::SamplingProfiler::totalTime):
     63        (JSC::SamplingProfiler::setStopWatch):
     64        (JSC::SamplingProfiler::stackTraces): Deleted.
     65        * tests/stress/sampling-profiler-anonymous-function.js:
     66        (platformSupportsSamplingProfiler.baz):
     67        (platformSupportsSamplingProfiler):
     68        * tests/stress/sampling-profiler-basic.js:
     69        (platformSupportsSamplingProfiler.nothing):
     70        (platformSupportsSamplingProfiler.top):
     71        * tests/stress/sampling-profiler/samplingProfiler.js:
     72        (doesTreeHaveStackTrace):
     73
    1742016-01-20  Keith Miller  <keith_miller@apple.com>
    275
  • trunk/Source/JavaScriptCore/debugger/Debugger.cpp

    r194704 r195376  
    235235}
    236236
    237 double Debugger::willEvaluateScript(JSGlobalObject& globalObject)
    238 {
    239     return m_profilingClient->willEvaluateScript(globalObject);
    240 }
    241 
    242 void Debugger::didEvaluateScript(JSGlobalObject& globalObject, double startTime, ProfilingReason reason)
    243 {
    244     m_profilingClient->didEvaluateScript(globalObject, startTime, reason);
     237double Debugger::willEvaluateScript()
     238{
     239    return m_profilingClient->willEvaluateScript();
     240}
     241
     242void Debugger::didEvaluateScript(double startTime, ProfilingReason reason)
     243{
     244    m_profilingClient->didEvaluateScript(startTime, reason);
    245245}
    246246
  • trunk/Source/JavaScriptCore/debugger/Debugger.h

    r194242 r195376  
    133133        virtual ~ProfilingClient() { }
    134134        virtual bool isAlreadyProfiling() const = 0;
    135         virtual double willEvaluateScript(JSGlobalObject&) = 0;
    136         virtual void didEvaluateScript(JSGlobalObject&, double startTime, ProfilingReason) = 0;
     135        virtual double willEvaluateScript() = 0;
     136        virtual void didEvaluateScript(double startTime, ProfilingReason) = 0;
    137137    };
    138138
     
    140140    bool hasProfilingClient() const { return m_profilingClient != nullptr; }
    141141    bool isAlreadyProfiling() const { return m_profilingClient && m_profilingClient->isAlreadyProfiling(); }
    142     double willEvaluateScript(JSGlobalObject&);
    143     void didEvaluateScript(JSGlobalObject&, double startTime, ProfilingReason);
     142    double willEvaluateScript();
     143    void didEvaluateScript(double startTime, ProfilingReason);
    144144
    145145protected:
  • trunk/Source/JavaScriptCore/debugger/ScriptProfilingScope.h

    r194242 r195376  
    4040    {
    4141        if (shouldStartProfile())
    42             m_startTime = m_globalObject->debugger()->willEvaluateScript(*m_globalObject);
     42            m_startTime = m_globalObject->debugger()->willEvaluateScript();
    4343    }
    4444
     
    4646    {
    4747        if (shouldEndProfile())
    48             m_globalObject->debugger()->didEvaluateScript(*m_globalObject, m_startTime.value(), m_reason);
     48            m_globalObject->debugger()->didEvaluateScript(m_startTime.value(), m_reason);
    4949    }
    5050
  • trunk/Source/JavaScriptCore/inspector/agents/InspectorScriptProfilerAgent.cpp

    r194496 r195376  
    2828
    2929#include "InspectorEnvironment.h"
    30 #include "LegacyProfiler.h"
     30#include "SamplingProfiler.h"
    3131#include <wtf/RunLoop.h>
    3232#include <wtf/Stopwatch.h>
     
    5858}
    5959
    60 void InspectorScriptProfilerAgent::startTracking(ErrorString&, const bool* profile)
     60void InspectorScriptProfilerAgent::startTracking(ErrorString&, const bool* includeSamples)
    6161{
    6262    if (m_tracking)
     
    6565    m_tracking = true;
    6666
    67     if (profile && *profile)
    68         m_enableLegacyProfiler = true;
     67#if ENABLE(SAMPLING_PROFILER)
     68    if (includeSamples && *includeSamples) {
     69        VM& vm = m_environment.scriptDebugServer().vm();
     70        vm.ensureSamplingProfiler(m_environment.executionStopwatch());
     71
     72        SamplingProfiler& samplingProfiler = *vm.samplingProfiler();
     73        LockHolder locker(samplingProfiler.getLock());
     74
     75        samplingProfiler.setStopWatch(locker, m_environment.executionStopwatch());
     76        samplingProfiler.noticeCurrentThreadAsJSCExecutionThread(locker);
     77        samplingProfiler.start(locker);
     78        m_enabledSamplingProfiler = true;
     79    }
     80#else
     81    UNUSED_PARAM(includeSamples);
     82#endif // ENABLE(SAMPLING_PROFILER)
    6983
    7084    m_environment.scriptDebugServer().setProfilingClient(this);
     
    7993
    8094    m_tracking = false;
    81     m_enableLegacyProfiler = false;
    8295    m_activeEvaluateScript = false;
    8396
     
    92105}
    93106
    94 double InspectorScriptProfilerAgent::willEvaluateScript(JSGlobalObject& globalObject)
     107double InspectorScriptProfilerAgent::willEvaluateScript()
    95108{
    96109    m_activeEvaluateScript = true;
    97110
    98     if (m_enableLegacyProfiler)
    99         LegacyProfiler::profiler()->startProfiling(globalObject.globalExec(), ASCIILiteral("ScriptProfiler"), m_environment.executionStopwatch());
     111#if ENABLE(SAMPLING_PROFILER)
     112    if (m_enabledSamplingProfiler) {
     113        SamplingProfiler* samplingProfiler = m_environment.scriptDebugServer().vm().samplingProfiler();
     114        RELEASE_ASSERT(samplingProfiler);
     115        samplingProfiler->noticeCurrentThreadAsJSCExecutionThread();
     116    }
     117#endif
    100118
    101119    return m_environment.executionStopwatch()->elapsedTime();
    102120}
    103121
    104 void InspectorScriptProfilerAgent::didEvaluateScript(JSGlobalObject& globalObject, double startTime, ProfilingReason reason)
     122void InspectorScriptProfilerAgent::didEvaluateScript(double startTime, ProfilingReason reason)
    105123{
    106124    m_activeEvaluateScript = false;
    107 
    108     if (m_enableLegacyProfiler)
    109         m_profiles.append(LegacyProfiler::profiler()->stopProfiling(globalObject.globalExec(), ASCIILiteral("ScriptProfiler")));
    110125
    111126    double endTime = m_environment.executionStopwatch()->elapsedTime();
     
    142157}
    143158
    144 static Ref<Protocol::Timeline::CPUProfileNodeAggregateCallInfo> buildAggregateCallInfoInspectorObject(const JSC::ProfileNode* node)
    145 {
    146     double startTime = node->calls()[0].startTime();
    147     double endTime = node->calls().last().startTime() + node->calls().last().elapsedTime();
    148 
    149     double totalTime = 0;
    150     for (const JSC::ProfileNode::Call& call : node->calls())
    151         totalTime += call.elapsedTime();
    152 
    153     return Protocol::Timeline::CPUProfileNodeAggregateCallInfo::create()
    154         .setCallCount(node->calls().size())
    155         .setStartTime(startTime)
    156         .setEndTime(endTime)
     159#if ENABLE(SAMPLING_PROFILER)
     160static Ref<Protocol::ScriptProfiler::Samples> buildSamples(Vector<SamplingProfiler::StackTrace>& samplingProfilerStackTraces, double totalTime)
     161{
     162    Ref<Protocol::Array<Protocol::ScriptProfiler::StackTrace>> stackTraces = Protocol::Array<Protocol::ScriptProfiler::StackTrace>::create();
     163    for (SamplingProfiler::StackTrace& stackTrace : samplingProfilerStackTraces) {
     164        Ref<Protocol::Array<Protocol::ScriptProfiler::StackFrame>> frames = Protocol::Array<Protocol::ScriptProfiler::StackFrame>::create();
     165        for (SamplingProfiler::StackFrame& stackFrame : stackTrace.frames) {
     166            Ref<Protocol::ScriptProfiler::StackFrame> frame = Protocol::ScriptProfiler::StackFrame::create()
     167                .setSourceID(String::number(stackFrame.sourceID()))
     168                .setName(stackFrame.displayName())
     169                .setLine(stackFrame.startLine())
     170                .setColumn(stackFrame.startColumn())
     171                .setUrl(stackFrame.url())
     172                .release();
     173            frames->addItem(WTFMove(frame));
     174        }
     175        Ref<Protocol::ScriptProfiler::StackTrace> inspectorStackTrace = Protocol::ScriptProfiler::StackTrace::create()
     176            .setTimestamp(stackTrace.timestamp)
     177            .setStackFrames(WTFMove(frames))
     178            .release();
     179        stackTraces->addItem(WTFMove(inspectorStackTrace));
     180    }
     181
     182    return Protocol::ScriptProfiler::Samples::create()
     183        .setStackTraces(WTFMove(stackTraces))
    157184        .setTotalTime(totalTime)
    158185        .release();
    159186}
    160 
    161 static Ref<Protocol::Timeline::CPUProfileNode> buildInspectorObject(const JSC::ProfileNode* node)
    162 {
    163     auto result = Protocol::Timeline::CPUProfileNode::create()
    164         .setId(node->id())
    165         .setCallInfo(buildAggregateCallInfoInspectorObject(node))
    166         .release();
    167 
    168     if (!node->functionName().isEmpty())
    169         result->setFunctionName(node->functionName());
    170 
    171     if (!node->url().isEmpty()) {
    172         result->setUrl(node->url());
    173         result->setLineNumber(node->lineNumber());
    174         result->setColumnNumber(node->columnNumber());
    175     }
    176 
    177     if (!node->children().isEmpty()) {
    178         auto children = Protocol::Array<Protocol::Timeline::CPUProfileNode>::create();
    179         for (RefPtr<JSC::ProfileNode> profileNode : node->children())
    180             children->addItem(buildInspectorObject(profileNode.get()));
    181         result->setChildren(WTFMove(children));
    182     }
    183 
    184     return result;
    185 }
    186 
    187 static Ref<Protocol::Timeline::CPUProfile> buildProfileInspectorObject(const JSC::Profile* profile)
    188 {
    189     auto rootNodes = Protocol::Array<Protocol::Timeline::CPUProfileNode>::create();
    190     for (RefPtr<JSC::ProfileNode> profileNode : profile->rootNode()->children())
    191         rootNodes->addItem(buildInspectorObject(profileNode.get()));
    192 
    193     return Protocol::Timeline::CPUProfile::create()
    194         .setRootNodes(WTFMove(rootNodes))
    195         .release();
    196 }
     187#endif // ENABLE(SAMPLING_PROFILER)
    197188
    198189void InspectorScriptProfilerAgent::trackingComplete()
    199190{
    200     RefPtr<Inspector::Protocol::Array<InspectorValue>> profiles = Inspector::Protocol::Array<InspectorValue>::create();
    201     for (auto& profile : m_profiles) {
    202         Ref<InspectorValue> value = buildProfileInspectorObject(profile.get());
    203         profiles->addItem(WTFMove(value));
    204     }
    205 
    206     m_frontendDispatcher->trackingComplete(profiles);
    207 
    208     m_profiles.clear();
     191#if ENABLE(SAMPLING_PROFILER)
     192    if (m_enabledSamplingProfiler) {
     193        SamplingProfiler* samplingProfiler = m_environment.scriptDebugServer().vm().samplingProfiler();
     194        RELEASE_ASSERT(samplingProfiler);
     195        LockHolder locker(samplingProfiler->getLock());
     196        samplingProfiler->stop(locker);
     197        Ref<Protocol::ScriptProfiler::Samples> samples = buildSamples(samplingProfiler->stackTraces(locker), samplingProfiler->totalTime(locker));
     198        samplingProfiler->clearData(locker);
     199
     200        locker.unlockEarly();
     201
     202        m_enabledSamplingProfiler = false;
     203
     204        m_frontendDispatcher->trackingComplete(WTFMove(samples));
     205    } else
     206        m_frontendDispatcher->trackingComplete(nullptr);
     207#else
     208    m_frontendDispatcher->trackingComplete(nullptr);
     209#endif // ENABLE(SAMPLING_PROFILER)
    209210}
    210211
  • trunk/Source/JavaScriptCore/inspector/agents/InspectorScriptProfilerAgent.h

    r194242 r195376  
    5151
    5252    // ScriptProfilerBackendDispatcherHandler
    53     virtual void startTracking(ErrorString&, const bool* profile) override;
     53    virtual void startTracking(ErrorString&, const bool* includeSamples) override;
    5454    virtual void stopTracking(ErrorString&) override;
    5555
    5656    // Debugger::ProfilingClient
    5757    virtual bool isAlreadyProfiling() const override;
    58     virtual double willEvaluateScript(JSC::JSGlobalObject&) override;
    59     virtual void didEvaluateScript(JSC::JSGlobalObject&, double, JSC::ProfilingReason) override;
     58    virtual double willEvaluateScript() override;
     59    virtual void didEvaluateScript(double, JSC::ProfilingReason) override;
    6060
    6161private:
     
    7171    std::unique_ptr<ScriptProfilerFrontendDispatcher> m_frontendDispatcher;
    7272    RefPtr<ScriptProfilerBackendDispatcher> m_backendDispatcher;
    73     Vector<RefPtr<JSC::Profile>> m_profiles;
    7473    InspectorEnvironment& m_environment;
    7574    bool m_tracking { false };
    76     bool m_enableLegacyProfiler { false };
     75    bool m_enabledSamplingProfiler { false };
    7776    bool m_activeEvaluateScript { false };
    7877};
  • trunk/Source/JavaScriptCore/inspector/protocol/ScriptProfiler.json

    r194242 r195376  
    1616                { "name": "type", "$ref": "EventType" }
    1717            ]
     18        },
     19        {
     20            "id": "StackFrame",
     21            "type": "object",
     22            "properties": [
     23                { "name": "sourceID", "$ref": "Debugger.ScriptId", "description": "Unique script identifier." },
     24                { "name": "name", "type": "string", "description": "A displayable name for the stack frame. i.e function name, (program), etc." },
     25                { "name": "line", "type": "integer" },
     26                { "name": "column", "type": "integer" },
     27                { "name": "url", "type": "string" }
     28            ]
     29        },
     30        {
     31            "id": "StackTrace",
     32            "type": "object",
     33            "properties": [
     34                { "name": "timestamp", "type": "number" },
     35                { "name": "stackFrames", "type": "array", "items": { "$ref": "StackFrame" }, "description": "First array item is the bottom of the call stack and last array item is the top of the call stack." }
     36            ]
     37        },
     38        {
     39            "id": "Samples",
     40            "type": "object",
     41            "properties": [
     42                { "name": "totalTime", "type": "number", "description": "Total execution time of the profiler's data. (Note: not total elapsed time.)" },
     43                { "name": "stackTraces", "type": "array", "items": { "$ref": "StackTrace" } }
     44            ]
    1845        }
    1946    ],
     
    2350            "description": "Start tracking script evaluations.",
    2451            "parameters": [
    25                 { "name": "profile", "type": "boolean", "optional": true, "description": "Profile script evaluations, defaults to false." }
     52                { "name": "includeSamples", "type": "boolean", "optional": true, "description": "Start the sampling profiler, defaults to false." }
    2653            ]
    2754        },
     
    5077            "description": "When tracking is complete the backend will send any buffered data, such as profiling information.",
    5178            "parameters": [
    52                 { "name": "profiles", "type": "array", "items": { "type": "any" }, "optional": true }
     79                { "name": "samples", "$ref": "Samples", "optional": true, "description": "Stack traces." }
    5380            ]
    5481        }
  • trunk/Source/JavaScriptCore/jsc.cpp

    r195233 r195376  
    16431643{
    16441644    RELEASE_ASSERT(exec->vm().samplingProfiler());
    1645     String jsonString = exec->vm().samplingProfiler()->stacktracesAsJSON();
     1645    String jsonString = exec->vm().samplingProfiler()->stackTracesAsJSON();
    16461646    exec->vm().samplingProfiler()->clearData();
    16471647    EncodedJSValue result = JSValue::encode(JSONParse(exec, jsonString));
  • trunk/Source/JavaScriptCore/runtime/SamplingProfiler.cpp

    r194840 r195376  
    406406{
    407407    LockHolder locker(m_lock);
     408    start(locker);
     409}
     410
     411void SamplingProfiler::start(const LockHolder& locker)
     412{
     413    ASSERT(m_lock.isLocked());
    408414    m_isActive = true;
    409415    dispatchIfNecessary(locker);
     
    413419{
    414420    LockHolder locker(m_lock);
     421    stop(locker);
     422}
     423
     424void SamplingProfiler::stop(const LockHolder&)
     425{
     426    ASSERT(m_lock.isLocked());
    415427    m_isActive = false;
    416428    reportStats();
     
    470482{
    471483    LockHolder locker(m_lock);
     484    clearData(locker);
     485}
     486
     487void SamplingProfiler::clearData(const LockHolder&)
     488{
     489    ASSERT(m_lock.isLocked());
    472490    m_stackTraces.clear();
    473491    m_seenExecutables.clear();
     
    475493}
    476494
    477 static String displayName(const SamplingProfiler::StackFrame& stackFrame)
    478 {
    479     if (stackFrame.frameType == FrameType::Unknown)
    480         return ASCIILiteral("<unknown>");
    481     if (stackFrame.frameType == FrameType::Host)
    482         return ASCIILiteral("<host>");
    483     RELEASE_ASSERT(stackFrame.frameType != FrameType::UnverifiedCallee);
    484 
    485     ExecutableBase* executable = stackFrame.u.verifiedExecutable;
     495String SamplingProfiler::StackFrame::displayName()
     496{
     497    if (frameType == FrameType::Unknown)
     498        return ASCIILiteral("(unknown)");
     499    if (frameType == FrameType::Host)
     500        return ASCIILiteral("(host)");
     501    RELEASE_ASSERT(frameType != FrameType::UnverifiedCallee);
     502
     503    ExecutableBase* executable = u.verifiedExecutable;
    486504    if (executable->isHostFunction())
    487         return ASCIILiteral("<host>");
     505        return static_cast<NativeExecutable*>(executable)->name();
     506
     507    if (executable->isFunctionExecutable())
     508        return static_cast<FunctionExecutable*>(executable)->inferredName().string();
     509    if (executable->isProgramExecutable() || executable->isEvalExecutable())
     510        return ASCIILiteral("(program)");
     511    if (executable->isModuleProgramExecutable())
     512        return ASCIILiteral("(module)");
     513
     514    RELEASE_ASSERT_NOT_REACHED();
     515    return String();
     516}
     517
     518String SamplingProfiler::StackFrame::displayNameForJSONTests()
     519{
     520    if (frameType == FrameType::Unknown)
     521        return ASCIILiteral("(unknown)");
     522    if (frameType == FrameType::Host)
     523        return ASCIILiteral("(host)");
     524    RELEASE_ASSERT(frameType != FrameType::UnverifiedCallee);
     525
     526    ExecutableBase* executable = u.verifiedExecutable;
     527    if (executable->isHostFunction())
     528        return static_cast<NativeExecutable*>(executable)->name();
    488529
    489530    if (executable->isFunctionExecutable()) {
    490531        String result = static_cast<FunctionExecutable*>(executable)->inferredName().string();
    491         if (!result.isEmpty())
    492             return result;
    493         return ASCIILiteral("<anonymous-function>");
     532        if (result.isEmpty())
     533            return ASCIILiteral("(anonymous function)");
     534        return result;
    494535    }
    495536    if (executable->isEvalExecutable())
    496         return ASCIILiteral("<eval>");
     537        return ASCIILiteral("(eval)");
    497538    if (executable->isProgramExecutable())
    498         return ASCIILiteral("<global>");
     539        return ASCIILiteral("(program)");
    499540    if (executable->isModuleProgramExecutable())
    500         return ASCIILiteral("<module>");
     541        return ASCIILiteral("(module)");
    501542
    502543    RELEASE_ASSERT_NOT_REACHED();
    503     return "";
    504 }
    505 
    506 String SamplingProfiler::stacktracesAsJSON()
    507 {
    508     m_lock.lock();
     544    return String();
     545}
     546
     547int SamplingProfiler::StackFrame::startLine()
     548{
     549    if (frameType == FrameType::Unknown || frameType == FrameType::Host)
     550        return -1;
     551    RELEASE_ASSERT(frameType != FrameType::UnverifiedCallee);
     552
     553    ExecutableBase* executable = u.verifiedExecutable;
     554    if (executable->isHostFunction())
     555        return -1;
     556    return static_cast<ScriptExecutable*>(executable)->firstLine();
     557}
     558
     559unsigned SamplingProfiler::StackFrame::startColumn()
     560{
     561    if (frameType == FrameType::Unknown || frameType == FrameType::Host)
     562        return -1;
     563    RELEASE_ASSERT(frameType != FrameType::UnverifiedCallee);
     564
     565    ExecutableBase* executable = u.verifiedExecutable;
     566    if (executable->isHostFunction())
     567        return -1;
     568
     569    return static_cast<ScriptExecutable*>(executable)->startColumn();
     570}
     571
     572intptr_t SamplingProfiler::StackFrame::sourceID()
     573{
     574    if (frameType == FrameType::Unknown || frameType == FrameType::Host)
     575        return -1;
     576    RELEASE_ASSERT(frameType != FrameType::UnverifiedCallee);
     577
     578    ExecutableBase* executable = u.verifiedExecutable;
     579    if (executable->isHostFunction())
     580        return -1;
     581
     582    return static_cast<ScriptExecutable*>(executable)->sourceID();
     583}
     584
     585String SamplingProfiler::StackFrame::url()
     586{
     587    if (frameType == FrameType::Unknown || frameType == FrameType::Host)
     588        return emptyString();
     589    RELEASE_ASSERT(frameType != FrameType::UnverifiedCallee);
     590
     591    ExecutableBase* executable = u.verifiedExecutable;
     592    if (executable->isHostFunction())
     593        return emptyString();
     594
     595    String url = static_cast<ScriptExecutable*>(executable)->sourceURL();
     596    if (url.isEmpty())
     597        return static_cast<ScriptExecutable*>(executable)->source().provider()->sourceURL(); // Fall back to sourceURL directive.
     598    return url;
     599}
     600
     601Vector<SamplingProfiler::StackTrace>& SamplingProfiler::stackTraces(const LockHolder&)
     602{
     603    ASSERT(m_lock.isLocked());
     604    {
     605        HeapIterationScope heapIterationScope(m_vm.heap);
     606        processUnverifiedStackTraces();
     607    }
     608
     609    return m_stackTraces;
     610}
     611
     612String SamplingProfiler::stackTracesAsJSON()
     613{
     614    LockHolder locker(m_lock);
     615
    509616    {
    510617        HeapIterationScope heapIterationScope(m_vm.heap);
     
    520627            json.appendLiteral(",");
    521628    };
    522     for (const StackTrace& stackTrace : m_stackTraces) {
     629    for (StackTrace& stackTrace : m_stackTraces) {
    523630        comma();
    524631        json.appendLiteral("[");
    525632        loopedOnce = false;
    526         for (const StackFrame& stackFrame : stackTrace.frames) {
     633        for (StackFrame& stackFrame : stackTrace.frames) {
    527634            comma();
    528635            json.appendLiteral("\"");
    529             json.append(displayName(stackFrame));
     636            json.append(stackFrame.displayNameForJSONTests());
    530637            json.appendLiteral("\"");
    531638            loopedOnce = true;
     
    536643
    537644    json.appendLiteral("]");
    538 
    539     m_lock.unlock();
    540645
    541646    return json.toString();
  • trunk/Source/JavaScriptCore/runtime/SamplingProfiler.h

    r194840 r195376  
    5151        Unknown
    5252    };
     53
    5354    struct StackFrame {
    5455        StackFrame(FrameType frameType, EncodedJSValue callee)
     
    7374            ExecutableBase* verifiedExecutable;
    7475        } u;
     76
     77        String displayName();
     78        String displayNameForJSONTests(); // Used for JSC stress tests because they want the "(anonymous function)" string for anonymous functions and they want "(eval)" for eval'd code.
     79        int startLine();
     80        unsigned startColumn();
     81        intptr_t sourceID();
     82        String url();
    7583    };
     84
    7685    struct StackTrace {
    7786        bool needsVerification;
     
    8998    void setTimingInterval(std::chrono::microseconds interval) { m_timingInterval = interval; }
    9099    JS_EXPORT_PRIVATE void start();
     100    void start(const LockHolder&);
    91101    void stop();
    92     const Vector<StackTrace>& stackTraces() const { return m_stackTraces; }
    93     JS_EXPORT_PRIVATE String stacktracesAsJSON();
     102    void stop(const LockHolder&);
     103    Vector<StackTrace>& stackTraces(const LockHolder&);
     104    JS_EXPORT_PRIVATE String stackTracesAsJSON();
    94105    JS_EXPORT_PRIVATE void noticeCurrentThreadAsJSCExecutionThread();
    95106    void noticeCurrentThreadAsJSCExecutionThread(const LockHolder&);
    96107    JS_EXPORT_PRIVATE void clearData();
     108    void clearData(const LockHolder&);
    97109    void processUnverifiedStackTraces(); // You should call this only after acquiring the lock.
     110    double totalTime(const LockHolder&) { return m_totalTime; }
     111    void setStopWatch(const LockHolder&, Ref<Stopwatch>&& stopwatch) { m_stopwatch = WTFMove(stopwatch); }
    98112
    99113private:
  • trunk/Source/JavaScriptCore/tests/stress/sampling-profiler-anonymous-function.js

    r194840 r195376  
    1919    }
    2020
    21     runTest(baz, ["<anonymous-function>", "foo", "baz"]);
     21    runTest(baz, ["(anonymous function)", "foo", "baz"]);
    2222}
  • trunk/Source/JavaScriptCore/tests/stress/sampling-profiler-basic.js

    r194840 r195376  
    1818    noInline(nothing);
    1919
    20     runTest(foo, ["<host>", "bar", "foo"]);
     20    runTest(foo, ["(host)", "bar", "foo"]);
    2121
    2222    function top() {
  • trunk/Source/JavaScriptCore/tests/stress/sampling-profiler/samplingProfiler.js

    r194840 r195376  
    3939    // call frame at index 0.
    4040    if (isRunFromRunTest)
    41         stackTrace = [...stackTrace, "runTest", "<global>"];
     41        stackTrace = [...stackTrace, "runTest", "(program)"];
    4242    else
    4343        stackTrace = [...stackTrace];
  • trunk/Source/WebInspectorUI/ChangeLog

    r195344 r195376  
     12016-01-20  Saam barati  <sbarati@apple.com>
     2
     3        Web Inspector: Hook the sampling profiler into the Timelines UI
     4        https://bugs.webkit.org/show_bug.cgi?id=152766
     5        <rdar://problem/24066360>
     6
     7        Reviewed by Joseph Pecoraro.
     8
     9        The main change in this patch is to swap in the SamplingProfiler
     10        in place of the LegacyProfiler. To do this, we've created a data
     11        structure called CallingContextTree which aggregates the SamplingProfiler's
     12        data into an easy to manage tree. To see how the data structure works,
     13        consider the following program:
     14        ```
     15        function bar() { // run code here for a long time. }
     16        function baz() { // run code here for a long time. }
     17        function foo() { bar(); baz(); }
     18        foo();
     19        ```
     20        From this program, we will create a tree like this:
     21                        (program)
     22                            |
     23                            |
     24                           foo
     25                           | |
     26                          /   \
     27                         /     \
     28                        bar     baz
     29       
     30        From this type of tree, we can easily create a CPUProfile payload
     31        object. Because the Timelines UI knows how to interact with the
     32        CPUProfile object and display it, we currently map the tree to this object
     33        to make it trivially easy to display the SamplingProfiler's data. In the future,
     34        we may want to find ways to work directly with the CallingContextTree instead
     35        of mapping it into another object.
     36
     37        * Localizations/en.lproj/localizedStrings.js:
     38        * UserInterface/Controllers/TimelineManager.js:
     39        * UserInterface/Main.html:
     40        * UserInterface/Models/CallingContextTree.js: Added.
     41        * UserInterface/Models/ScriptInstrument.js:
     42        * UserInterface/Protocol/ScriptProfilerObserver.js:
     43        * UserInterface/TestStub.html:
     44        * UserInterface/Views/ScriptTimelineView.js:
     45
    1462016-01-19  Joseph Pecoraro  <pecoraro@apple.com>
    247
  • trunk/Source/WebInspectorUI/Localizations/en.lproj/localizedStrings.js

    r194977 r195376  
    545545localizedStrings["Right"] = "Right";
    546546localizedStrings["Role"] = "Role";
     547localizedStrings["Samples"] = "Samples";
    547548localizedStrings["Scheme"] = "Scheme";
    548549localizedStrings["Scope Chain"] = "Scope Chain";
  • trunk/Source/WebInspectorUI/UserInterface/Controllers/TimelineManager.js

    r194871 r195376  
    4646        this._scriptProfilerRecords = null;
    4747
     48        this._callingContextTree = null;
     49
    4850        this.reset();
    4951    }
     
    683685    }
    684686
    685     scriptProfilerTrackingCompleted(profiles)
     687    scriptProfilerTrackingCompleted(samples)
    686688    {
    687689        console.assert(!this._webTimelineScriptRecordsExpectingScriptProfilerEvents || this._scriptProfilerRecords.length >= this._webTimelineScriptRecordsExpectingScriptProfilerEvents.length);
    688690
    689         // Associate the profiles with the ScriptProfiler created records.
    690         if (profiles) {
    691             console.assert(this._scriptProfilerRecords.length === profiles.length, this._scriptProfilerRecords.length, profiles.length);
    692             for (let i = 0; i < this._scriptProfilerRecords.length; ++i)
    693                 this._scriptProfilerRecords[i].profilePayload = profiles[i];
     691        if (samples) {
     692            if (!this._callingContextTree)
     693                this._callingContextTree = new WebInspector.CallingContextTree;
     694
     695            // Associate the stackTraces with the ScriptProfiler created records.
     696            let stackTraces = samples.stackTraces;
     697            for (let i = 0; i < stackTraces.length; i++)
     698                this._callingContextTree.updateTreeWithStackTrace(stackTraces[i]);
     699
     700            for (let i = 0; i < this._scriptProfilerRecords.length; ++i) {
     701                let record = this._scriptProfilerRecords[i];
     702                record.profilePayload = this._callingContextTree.toCPUProfilePayload(record.startTime, record.endTime);
     703            }
    694704        }
    695705
  • trunk/Source/WebInspectorUI/UserInterface/Main.html

    r195305 r195376  
    270270    <script src="Models/Branch.js"></script>
    271271    <script src="Models/Breakpoint.js"></script>
     272    <script src="Models/CallingContextTree.js"></script>
    272273    <script src="Models/CSSCompletions.js"></script>
    273274    <script src="Models/CSSKeywordCompletions.js"></script>
  • trunk/Source/WebInspectorUI/UserInterface/Models/ScriptInstrument.js

    r194871 r195376  
    4242
    4343        // FIXME: Make this some UI visible option.
    44         const includeProfiles = true;
     44        const includeSamples = true;
    4545
    46         ScriptProfilerAgent.startTracking(includeProfiles);
     46        ScriptProfilerAgent.startTracking(includeSamples);
    4747    }
    4848
  • trunk/Source/WebInspectorUI/UserInterface/Protocol/ScriptProfilerObserver.js

    r194242 r195376  
    3838    }
    3939
    40     trackingComplete(profiles)
     40    trackingComplete(samples)
    4141    {
    42         WebInspector.timelineManager.scriptProfilerTrackingCompleted(profiles);
     42        WebInspector.timelineManager.scriptProfilerTrackingCompleted(samples);
    4343    }
    4444};
  • trunk/Source/WebInspectorUI/UserInterface/TestStub.html

    r195305 r195376  
    3434    <script src="Base/ListMultimap.js"></script>
    3535    <script src="Base/Object.js"></script>
     36    <script src="Base/Utilities.js"></script>
     37
     38    <script src="Models/CallingContextTree.js"></script>
    3639
    3740    <script src="Test/TestSuite.js"></script>
  • trunk/Source/WebInspectorUI/UserInterface/Views/ScriptTimelineView.js

    r194878 r195376  
    3939        columns.location.width = "15%";
    4040
    41         columns.callCount.title = WebInspector.UIString("Calls");
     41        let isSamplingProfiler = !!window.ScriptProfilerAgent;
     42        if (isSamplingProfiler)
     43            columns.callCount.title = WebInspector.UIString("Samples");
     44        else {
     45            // COMPATIBILITY(iOS 9): ScriptProfilerAgent did not exist yet, we had call counts, not samples.
     46            columns.callCount.title = WebInspector.UIString("Calls");
     47        }
    4248        columns.callCount.width = "5%";
    4349        columns.callCount.aligned = "right";
Note: See TracChangeset for help on using the changeset viewer.