1
0
forked from 0ad/0ad

Revamp Profiler2 to make it more usable.

Features include new graphs to compare runtime of functions and runtimes
across reports, as well as new profiling functions that only profile
spikes.

This was SVN commit r18423.
This commit is contained in:
wraitii 2016-06-22 13:38:05 +00:00
parent 68bc88fb95
commit 3cbe96d24c
10 changed files with 2124 additions and 859 deletions

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2014 Wildfire Games
/* Copyright (c) 2016 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -31,6 +31,7 @@
#include "third_party/mongoose/mongoose.h"
#include <iomanip>
#include <unordered_map>
CProfiler2 g_Profiler2;
@ -80,7 +81,12 @@ static void* MgCallback(mg_event event, struct mg_connection *conn, const struct
std::stringstream stream;
std::string uri = request_info->uri;
if (uri == "/overview")
if (uri == "/download")
{
profiler->SaveToFile();
}
else if (uri == "/overview")
{
profiler->ConstructJSONOverview(stream);
}
@ -301,7 +307,7 @@ void CProfiler2::RemoveThreadStorage(ThreadStorage* storage)
}
CProfiler2::ThreadStorage::ThreadStorage(CProfiler2& profiler, const std::string& name) :
m_Profiler(profiler), m_Name(name), m_BufferPos0(0), m_BufferPos1(0), m_LastTime(timer_Time())
m_Profiler(profiler), m_Name(name), m_BufferPos0(0), m_BufferPos1(0), m_LastTime(timer_Time()), m_HeldDepth(0)
{
m_Buffer = new u8[BUFFER_SIZE];
memset(m_Buffer, ITEM_NOP, BUFFER_SIZE);
@ -312,6 +318,55 @@ CProfiler2::ThreadStorage::~ThreadStorage()
delete[] m_Buffer;
}
void CProfiler2::ThreadStorage::Write(EItem type, const void* item, u32 itemSize)
{
if (m_HeldDepth > 0)
{
WriteHold(type, item, itemSize);
return;
}
// See m_BufferPos0 etc for comments on synchronisation
u32 size = 1 + itemSize;
u32 start = m_BufferPos0;
if (start + size > BUFFER_SIZE)
{
// The remainder of the buffer is too small - fill the rest
// with NOPs then start from offset 0, so we don't have to
// bother splitting the real item across the end of the buffer
m_BufferPos0 = size;
COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer
memset(m_Buffer + start, 0, BUFFER_SIZE - start);
start = 0;
}
else
{
m_BufferPos0 = start + size;
COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer
}
m_Buffer[start] = (u8)type;
memcpy(&m_Buffer[start + 1], item, itemSize);
COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer
m_BufferPos1 = start + size;
}
void CProfiler2::ThreadStorage::WriteHold(EItem type, const void* item, u32 itemSize)
{
u32 size = 1 + itemSize;
if (m_HoldBuffers[m_HeldDepth - 1].pos + size > CProfiler2::HOLD_BUFFER_SIZE)
return; // we held on too much data, ignore the rest
m_HoldBuffers[m_HeldDepth - 1].buffer[m_HoldBuffers[m_HeldDepth - 1].pos] = (u8)type;
memcpy(&m_HoldBuffers[m_HeldDepth - 1].buffer[m_HoldBuffers[m_HeldDepth - 1].pos + 1], item, itemSize);
m_HoldBuffers[m_HeldDepth - 1].pos += size;
}
std::string CProfiler2::ThreadStorage::GetBuffer()
{
// Called from an arbitrary thread (not the one writing to the buffer).
@ -355,6 +410,314 @@ void CProfiler2::ThreadStorage::RecordAttribute(const char* fmt, va_list argp)
Write(ITEM_ATTRIBUTE, buffer, 4 + len);
}
size_t CProfiler2::ThreadStorage::HoldLevel()
{
return m_HeldDepth;
}
u8 CProfiler2::ThreadStorage::HoldType()
{
return m_HoldBuffers[m_HeldDepth - 1].type;
}
void CProfiler2::ThreadStorage::PutOnHold(u8 newType)
{
m_HeldDepth++;
m_HoldBuffers[m_HeldDepth - 1].clear();
m_HoldBuffers[m_HeldDepth - 1].setType(newType);
}
// this flattens the stack, use it sensibly
void rewriteBuffer(u8* buffer, u32& bufferSize)
{
double startTime = timer_Time();
u32 size = bufferSize;
u32 readPos = 0;
double initialTime = -1;
double total_time = -1;
const char* regionName;
std::set<std::string> topLevelArgs;
typedef std::tuple<const char*, double, std::set<std::string> > infoPerType;
std::unordered_map<std::string, infoPerType> timeByType;
std::vector<double> last_time_stack;
std::vector<const char*> last_names;
// never too many hacks
std::string current_attribute = "";
std::map<std::string, double> time_per_attribute;
// Let's read the first event
{
u8 type = buffer[readPos];
++readPos;
if (type != CProfiler2::ITEM_ENTER)
{
debug_warn("Profiler2: Condensing a region should run into ITEM_ENTER first");
return; // do nothing
}
CProfiler2::SItem_dt_id item;
memcpy(&item, buffer + readPos, sizeof(item));
readPos += sizeof(item);
regionName = item.id;
last_names.push_back(item.id);
initialTime = (double)item.dt;
}
int enter = 1;
int leaves = 0;
// Read subsequent events. Flatten hierarchy because it would get too complicated otherwise.
// To make sure time doesn't bloat, subtract time from nested events
while (readPos < size)
{
u8 type = buffer[readPos];
++readPos;
switch (type)
{
case CProfiler2::ITEM_NOP:
{
// ignore
break;
}
case CProfiler2::ITEM_SYNC:
{
debug_warn("Aggregated regions should not be used across frames");
// still try to act sane
readPos += sizeof(double);
readPos += sizeof(CProfiler2::RESYNC_MAGIC);
break;
}
case CProfiler2::ITEM_EVENT:
{
// skip for now
readPos += sizeof(CProfiler2::SItem_dt_id);
break;
}
case CProfiler2::ITEM_ENTER:
{
enter++;
CProfiler2::SItem_dt_id item;
memcpy(&item, buffer + readPos, sizeof(item));
readPos += sizeof(item);
last_time_stack.push_back((double)item.dt);
last_names.push_back(item.id);
current_attribute = "";
break;
}
case CProfiler2::ITEM_LEAVE:
{
float item_time;
memcpy(&item_time, buffer + readPos, sizeof(float));
readPos += sizeof(float);
leaves++;
if (last_names.empty())
{
// we somehow lost the first entry in the process
debug_warn("Invalid buffer for condensing");
}
const char* item_name = last_names.back();
last_names.pop_back();
if (last_time_stack.empty())
{
// this is the leave for the whole scope
total_time = (double)item_time;
break;
}
double time = (double)item_time - last_time_stack.back();
std::string name = std::string(item_name);
auto TimeForType = timeByType.find(name);
if (TimeForType == timeByType.end())
{
// keep reference to the original char pointer to make sure we don't break things down the line
std::get<0>(timeByType[name]) = item_name;
std::get<1>(timeByType[name]) = 0;
}
std::get<1>(timeByType[name]) += time;
last_time_stack.pop_back();
// if we were nested, subtract our time from the below scope by making it look like it starts later
if (!last_time_stack.empty())
last_time_stack.back() += time;
if (!current_attribute.empty())
{
time_per_attribute[current_attribute] += time;
}
break;
}
case CProfiler2::ITEM_ATTRIBUTE:
{
// skip for now
u32 len;
memcpy(&len, buffer + readPos, sizeof(len));
ENSURE(len <= CProfiler2::MAX_ATTRIBUTE_LENGTH);
readPos += sizeof(len);
char message[CProfiler2::MAX_ATTRIBUTE_LENGTH] = {0};
memcpy(&message[0], buffer + readPos, len);
CStr mess = CStr((const char*)message, len);
if (!last_names.empty())
{
auto it = timeByType.find(std::string(last_names.back()));
if (it == timeByType.end())
topLevelArgs.insert(mess);
else
std::get<2>(timeByType[std::string(last_names.back())]).insert(mess);
}
readPos += len;
current_attribute = mess;
break;
}
default:
debug_warn(L"Invalid profiler item when condensing buffer");
continue;
}
}
// rewrite the buffer
// what we rewrite will always be smaller than the current buffer's size
u32 writePos = 0;
double curTime = initialTime;
// the region enter
{
CProfiler2::SItem_dt_id item = { curTime, regionName };
buffer[writePos] = (u8)CProfiler2::ITEM_ENTER;
memcpy(buffer + writePos + 1, &item, sizeof(item));
writePos += sizeof(item) + 1;
// add a nanosecond for sanity
curTime += 0.000001;
}
// sub-events, aggregated
for (auto& type : timeByType)
{
CProfiler2::SItem_dt_id item = { curTime, std::get<0>(type.second) };
buffer[writePos] = (u8)CProfiler2::ITEM_ENTER;
memcpy(buffer + writePos + 1, &item, sizeof(item));
writePos += sizeof(item) + 1;
// write relevant attributes if present
for (const auto& attrib : std::get<2>(type.second))
{
buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE;
writePos++;
std::string basic = attrib;
auto time_attrib = time_per_attribute.find(attrib);
if (time_attrib != time_per_attribute.end())
basic += " " + CStr::FromInt(1000000*time_attrib->second) + "us";
u32 length = basic.size();
memcpy(buffer + writePos, &length, sizeof(length));
writePos += sizeof(length);
memcpy(buffer + writePos, basic.c_str(), length);
writePos += length;
}
curTime += std::get<1>(type.second);
float leave_time = (float)curTime;
buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE;
memcpy(buffer + writePos + 1, &leave_time, sizeof(float));
writePos += sizeof(float) + 1;
}
// Time of computation
{
CProfiler2::SItem_dt_id item = { curTime, "CondenseBuffer" };
buffer[writePos] = (u8)CProfiler2::ITEM_ENTER;
memcpy(buffer + writePos + 1, &item, sizeof(item));
writePos += sizeof(item) + 1;
}
{
float time_out = (float)(curTime + timer_Time() - startTime);
buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE;
memcpy(buffer + writePos + 1, &time_out, sizeof(float));
writePos += sizeof(float) + 1;
// add a nanosecond for sanity
curTime += 0.000001;
}
// the region leave
{
if (total_time < 0)
{
total_time = curTime + 0.000001;
buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE;
writePos++;
u32 length = sizeof("buffer overflow");
memcpy(buffer + writePos, &length, sizeof(length));
writePos += sizeof(length);
memcpy(buffer + writePos, "buffer overflow", length);
writePos += length;
}
else if (total_time < curTime)
{
// this seems to happen on rare occasions.
curTime = total_time;
}
float leave_time = (float)total_time;
buffer[writePos] = (u8)CProfiler2::ITEM_LEAVE;
memcpy(buffer + writePos + 1, &leave_time, sizeof(float));
writePos += sizeof(float) + 1;
}
bufferSize = writePos;
}
void CProfiler2::ThreadStorage::HoldToBuffer(bool condensed)
{
ENSURE(m_HeldDepth);
if (condensed)
{
// rewrite the buffer to show aggregated data
rewriteBuffer(m_HoldBuffers[m_HeldDepth - 1].buffer, m_HoldBuffers[m_HeldDepth - 1].pos);
}
if (m_HeldDepth > 1)
{
// copy onto buffer below
HoldBuffer& copied = m_HoldBuffers[m_HeldDepth - 1];
HoldBuffer& target = m_HoldBuffers[m_HeldDepth - 2];
if (target.pos + copied.pos > HOLD_BUFFER_SIZE)
return; // too much data, too bad
memcpy(&target.buffer[target.pos], copied.buffer, copied.pos);
target.pos += copied.pos;
}
else
{
u32 size = m_HoldBuffers[m_HeldDepth - 1].pos;
u32 start = m_BufferPos0;
if (start + size > BUFFER_SIZE)
{
m_BufferPos0 = size;
COMPILER_FENCE;
memset(m_Buffer + start, 0, BUFFER_SIZE - start);
start = 0;
}
else
{
m_BufferPos0 = start + size;
COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer
}
memcpy(&m_Buffer[start], m_HoldBuffers[m_HeldDepth - 1].buffer, size);
COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer
m_BufferPos1 = start + size;
}
m_HeldDepth--;
}
void CProfiler2::ThreadStorage::ThrowawayHoldBuffer()
{
if (!m_HeldDepth)
return;
m_HeldDepth--;
}
void CProfiler2::ConstructJSONOverview(std::ostream& stream)
{
@ -438,8 +801,7 @@ void RunBufferVisitor(const std::string& buffer, V& visitor)
pos += sizeof(item);
if (lastTime >= 0)
{
lastTime = lastTime + (double)item.dt;
visitor.OnEvent(lastTime, item.id);
visitor.OnEvent(lastTime + (double)item.dt, item.id);
}
break;
}
@ -450,20 +812,18 @@ void RunBufferVisitor(const std::string& buffer, V& visitor)
pos += sizeof(item);
if (lastTime >= 0)
{
lastTime = lastTime + (double)item.dt;
visitor.OnEnter(lastTime, item.id);
visitor.OnEnter(lastTime + (double)item.dt, item.id);
}
break;
}
case CProfiler2::ITEM_LEAVE:
{
CProfiler2::SItem_dt_id item;
memcpy(&item, buffer.c_str()+pos, sizeof(item));
pos += sizeof(item);
float leave_time;
memcpy(&leave_time, buffer.c_str() + pos, sizeof(float));
pos += sizeof(float);
if (lastTime >= 0)
{
lastTime = lastTime + (double)item.dt;
visitor.OnLeave(lastTime, item.id);
visitor.OnLeave(lastTime + (double)leave_time);
}
break;
}
@ -519,10 +879,9 @@ public:
m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n";
}
void OnLeave(double time, const char* id)
void OnLeave(double time)
{
m_Stream << "[3," << std::fixed << std::setprecision(9) << time;
m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n";
m_Stream << "[3," << std::fixed << std::setprecision(9) << time << "],\n";
}
void OnAttribute(const std::string& attr)
@ -596,3 +955,45 @@ void CProfiler2::SaveToFile()
}
stream << "\n]});\n";
}
CProfile2SpikeRegion::CProfile2SpikeRegion(const char* name, double spikeLimit) :
m_Name(name), m_Limit(spikeLimit), m_PushedHold(true)
{
if (g_Profiler2.HoldLevel() < 8 && g_Profiler2.HoldType() != CProfiler2::ThreadStorage::BUFFER_AGGREGATE)
g_Profiler2.HoldMessages(CProfiler2::ThreadStorage::BUFFER_SPIKE);
else
m_PushedHold = false;
COMPILER_FENCE;
g_Profiler2.RecordRegionEnter(m_Name);
m_StartTime = g_Profiler2.GetTime();
}
CProfile2SpikeRegion::~CProfile2SpikeRegion()
{
double time = g_Profiler2.GetTime();
g_Profiler2.RecordRegionLeave();
bool shouldWrite = time - m_StartTime > m_Limit;
if (m_PushedHold)
g_Profiler2.StopHoldingMessages(shouldWrite);
}
CProfile2AggregatedRegion::CProfile2AggregatedRegion(const char* name, double spikeLimit) :
m_Name(name), m_Limit(spikeLimit), m_PushedHold(true)
{
if (g_Profiler2.HoldLevel() < 8 && g_Profiler2.HoldType() != CProfiler2::ThreadStorage::BUFFER_AGGREGATE)
g_Profiler2.HoldMessages(CProfiler2::ThreadStorage::BUFFER_AGGREGATE);
else
m_PushedHold = false;
COMPILER_FENCE;
g_Profiler2.RecordRegionEnter(m_Name);
m_StartTime = g_Profiler2.GetTime();
}
CProfile2AggregatedRegion::~CProfile2AggregatedRegion()
{
double time = g_Profiler2.GetTime();
g_Profiler2.RecordRegionLeave();
bool shouldWrite = time - m_StartTime > m_Limit;
if (m_PushedHold)
g_Profiler2.StopHoldingMessages(shouldWrite, true);
}

View File

@ -1,4 +1,4 @@
/* Copyright (c) 2014 Wildfire Games
/* Copyright (c) 2016 Wildfire Games
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -91,7 +91,8 @@ class CProfiler2GPU;
class CProfiler2
{
friend class CProfiler2GPU_base;
friend class CProfile2SpikeRegion;
friend class CProfile2AggregatedRegion;
public:
// Items stored in the buffers:
@ -123,7 +124,8 @@ public:
private:
// TODO: what's a good size?
// TODO: different threads might want different sizes
static const size_t BUFFER_SIZE = 1024*1024;
static const size_t BUFFER_SIZE = 4*1024*1024;
static const size_t HOLD_BUFFER_SIZE = 128 * 1024;
/**
* Class instantiated in every registered thread.
@ -135,6 +137,8 @@ private:
ThreadStorage(CProfiler2& profiler, const std::string& name);
~ThreadStorage();
enum { BUFFER_NORMAL, BUFFER_SPIKE, BUFFER_AGGREGATE };
void RecordSyncMarker(double t)
{
// Store the magic string followed by the absolute time
@ -153,7 +157,6 @@ private:
// (to save memory) without suffering from precision problems
SItem_dt_id item = { (float)(t - m_LastTime), id };
Write(type, &item, sizeof(item));
m_LastTime = t;
}
void RecordFrameStart(double t)
@ -162,6 +165,12 @@ private:
Record(ITEM_EVENT, t, "__framestart"); // magic string recognised by the visualiser
}
void RecordLeave(double t)
{
float time = (float)(t - m_LastTime);
Write(ITEM_LEAVE, &time, sizeof(float));
}
void RecordAttribute(const char* fmt, va_list argp) VPRINTF_ARGS(2);
void RecordAttributePrintf(const char* fmt, ...) PRINTF_ARGS(2)
@ -172,6 +181,12 @@ private:
va_end(argp);
}
size_t HoldLevel();
u8 HoldType();
void PutOnHold(u8 type);
void HoldToBuffer(bool condensed);
void ThrowawayHoldBuffer();
CProfiler2& GetProfiler()
{
return m_Profiler;
@ -193,36 +208,9 @@ private:
/**
* Store an item into the buffer.
*/
void Write(EItem type, const void* item, u32 itemSize)
{
// See m_BufferPos0 etc for comments on synchronisation
void Write(EItem type, const void* item, u32 itemSize);
u32 size = 1 + itemSize;
u32 start = m_BufferPos0;
if (start + size > BUFFER_SIZE)
{
// The remainder of the buffer is too small - fill the rest
// with NOPs then start from offset 0, so we don't have to
// bother splitting the real item across the end of the buffer
m_BufferPos0 = size;
COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer
memset(m_Buffer + start, 0, BUFFER_SIZE - start);
start = 0;
}
else
{
m_BufferPos0 = start + size;
COMPILER_FENCE; // must write m_BufferPos0 before m_Buffer
}
m_Buffer[start] = (u8)type;
memcpy(&m_Buffer[start + 1], item, itemSize);
COMPILER_FENCE; // must write m_BufferPos1 after m_Buffer
m_BufferPos1 = start + size;
}
void WriteHold(EItem type, const void* item, u32 itemSize);
CProfiler2& m_Profiler;
std::string m_Name;
@ -231,6 +219,36 @@ private:
u8* m_Buffer;
struct HoldBuffer
{
friend class ThreadStorage;
public:
HoldBuffer()
{
buffer = new u8[HOLD_BUFFER_SIZE];
memset(buffer, ITEM_NOP, HOLD_BUFFER_SIZE);
pos = 0;
}
~HoldBuffer()
{
delete[] buffer;
}
void clear()
{
pos = 0;
}
void setType(u8 newType)
{
type = newType;
}
u8* buffer;
u32 pos;
u8 type;
};
HoldBuffer m_HoldBuffers[8];
size_t m_HeldDepth;
// To allow hopefully-safe reading of the buffer from a separate thread,
// without any expensive synchronisation in the recording thread,
// two copies of the current buffer write position are stored.
@ -327,9 +345,14 @@ public:
GetThreadStorage().Record(ITEM_ENTER, GetTime(), id);
}
void RecordRegionLeave(const char* id)
void RecordRegionEnter(const char* id, double time)
{
GetThreadStorage().Record(ITEM_LEAVE, GetTime(), id);
GetThreadStorage().Record(ITEM_ENTER, time, id);
}
void RecordRegionLeave()
{
GetThreadStorage().RecordLeave(GetTime());
}
void RecordAttribute(const char* fmt, ...) PRINTF_ARGS(2)
@ -345,6 +368,32 @@ public:
void RecordGPURegionEnter(const char* id);
void RecordGPURegionLeave(const char* id);
/**
* Hold onto messages until a call to release or write the held messages.
*/
size_t HoldLevel()
{
return GetThreadStorage().HoldLevel();
}
u8 HoldType()
{
return GetThreadStorage().HoldType();
}
void HoldMessages(u8 type)
{
GetThreadStorage().PutOnHold(type);
}
void StopHoldingMessages(bool writeToBuffer, bool condensed = false)
{
if (writeToBuffer)
GetThreadStorage().HoldToBuffer(condensed);
else
GetThreadStorage().ThrowawayHoldBuffer();
}
/**
* Call in any thread to produce a JSON representation of the general
* state of the application.
@ -422,10 +471,40 @@ public:
}
~CProfile2Region()
{
g_Profiler2.RecordRegionLeave(m_Name);
g_Profiler2.RecordRegionLeave();
}
protected:
const char* m_Name;
};
/**
* Scope-based enter/leave helper.
*/
class CProfile2SpikeRegion
{
public:
CProfile2SpikeRegion(const char* name, double spikeLimit);
~CProfile2SpikeRegion();
private:
const char* m_Name;
double m_Limit;
double m_StartTime;
bool m_PushedHold;
};
/**
* Scope-based enter/leave helper.
*/
class CProfile2AggregatedRegion
{
public:
CProfile2AggregatedRegion(const char* name, double spikeLimit);
~CProfile2AggregatedRegion();
private:
const char* m_Name;
double m_Limit;
double m_StartTime;
bool m_PushedHold;
};
/**
@ -455,6 +534,10 @@ private:
*/
#define PROFILE2(region) CProfile2Region profile2__(region)
#define PROFILE2_IFSPIKE(region, limit) CProfile2SpikeRegion profile2__(region, limit)
#define PROFILE2_AGGREGATED(region, limit) CProfile2AggregatedRegion profile2__(region, limit)
#define PROFILE2_GPU(region) CProfile2GPURegion profile2gpu__(region)
/**

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2014 Wildfire Games.
/* Copyright (C) 2016 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -278,7 +278,7 @@ private:
// Wait until the main thread wakes us up
while (SDL_SemWait(m_WorkerSem) == 0)
{
g_Profiler2.RecordRegionLeave("semaphore wait");
g_Profiler2.RecordRegionLeave();
// Handle shutdown requests as soon as possible
if (GetShutdown())
@ -302,7 +302,7 @@ private:
}
}
g_Profiler2.RecordRegionLeave("semaphore wait");
g_Profiler2.RecordRegionLeave();
}
bool GetEnabled()

View File

@ -262,7 +262,7 @@ bool ProfileStop(JSContext* UNUSED(cx), uint UNUSED(argc), jsval* vp)
if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread())
g_Profiler.Stop();
g_Profiler2.RecordRegionLeave("(ProfileStop)");
g_Profiler2.RecordRegionLeave();
rec.rval().setUndefined();
return true;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2015 Wildfire Games.
/* Copyright (C) 2016 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -46,7 +46,7 @@ void GCSliceCallbackHook(JSRuntime* UNUSED(rt), JS::GCProgress progress, const J
{
if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread())
g_Profiler.Stop();
g_Profiler2.RecordRegionLeave("GCSlice");
g_Profiler2.RecordRegionLeave();
}
else if (progress == JS::GC_CYCLE_BEGIN)
{
@ -58,7 +58,7 @@ void GCSliceCallbackHook(JSRuntime* UNUSED(rt), JS::GCProgress progress, const J
{
if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread())
g_Profiler.Stop();
g_Profiler2.RecordRegionLeave("GCSlice");
g_Profiler2.RecordRegionLeave();
}
// The following code can be used to print some information aobut garbage collection

View File

@ -0,0 +1,418 @@
// Copyright (c) 2016 Wildfire Games
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Profiler2Report module
// Create one instance per profiler report you wish to open.
// This gives you the interface to access the raw and processed data
var Profiler2Report = function(callback, tryLive, file)
{
var outInterface = {};
// Item types returned by the engine
var ITEM_EVENT = 1;
var ITEM_ENTER = 2;
var ITEM_LEAVE = 3;
var ITEM_ATTRIBUTE = 4;
var g_used_colours = {};
var g_raw_data;
var g_data;
function refresh(callback, tryLive, file)
{
if (tryLive)
refresh_live(callback, file);
else
refresh_jsonp(callback, file);
}
outInterface.refresh = refresh;
function refresh_jsonp(callback, source)
{
if (!source)
{
callback(false);
return
}
var reader = new FileReader();
reader.onload = function(e)
{
refresh_from_jsonp(callback, e.target.result);
}
reader.onerror = function(e) {
alert("Failed to load report file");
callback(false);
return;
}
reader.readAsText(source);
}
function refresh_from_jsonp(callback, content)
{
var script = document.createElement('script');
window.profileDataCB = function(data)
{
script.parentNode.removeChild(script);
var threads = [];
data.threads.forEach(function(thread) {
var canvas = $('<canvas width="1600" height="160"></canvas>');
threads.push({'name': thread.name, 'data': { 'events': concat_events(thread.data) }, 'canvas': canvas.get(0)});
});
g_raw_data = { 'threads': threads };
compute_data();
callback(true);
};
script.innerHTML = content;
document.body.appendChild(script);
}
function refresh_live(callback, file)
{
$.ajax({
url: 'http://127.0.0.1:8000/overview',
dataType: 'json',
success: function (data) {
var threads = [];
data.threads.forEach(function(thread) {
threads.push({'name': thread.name});
});
var callback_data = { 'threads': threads, 'completed': 0 };
threads.forEach(function(thread) {
refresh_thread(callback, thread, callback_data);
});
},
error: function (jqXHR, textStatus, errorThrown)
{
console.log('Failed to connect to server ("'+textStatus+'")');
callback(false);
}
});
}
function refresh_thread(callback, thread, callback_data)
{
$.ajax({
url: 'http://127.0.0.1:8000/query',
dataType: 'json',
data: { 'thread': thread.name },
success: function (data) {
data.events = concat_events(data);
thread.data = data;
if (++callback_data.completed == callback_data.threads.length)
{
g_raw_data = { 'threads': callback_data.threads };
compute_data();
callback(true);
}
},
error: function (jqXHR, textStatus, errorThrown) {
alert('Failed to connect to server ("'+textStatus+'")');
}
});
}
function compute_data(range)
{
g_data = { "threads" : [] };
g_data_by_frame = { "threads" : [] };
for (var thread = 0; thread < g_raw_data.threads.length; thread++)
{
g_data.threads[thread] = process_raw_data(g_raw_data.threads[thread].data.events, range );
g_data.threads[thread].intervals_by_type_frame = {};
for (let type in g_data.threads[thread].intervals_by_type)
{
let current_frame = 0;
g_data.threads[thread].intervals_by_type_frame[type] = [[]];
for (let i = 0; i < g_data.threads[thread].intervals_by_type[type].length;i++)
{
let event = g_data.threads[thread].intervals[g_data.threads[thread].intervals_by_type[type][i]];
while (event.t0 > g_data.threads[thread].frames[current_frame].t1 && current_frame < g_data.threads[thread].frames.length)
{
g_data.threads[thread].intervals_by_type_frame[type].push([]);
current_frame++;
}
g_data.threads[thread].intervals_by_type_frame[type][current_frame].push(g_data.threads[thread].intervals_by_type[type][i]);
}
}
};
}
function process_raw_data(data, range)
{
var start, end;
var tmin, tmax;
var frames = [];
var last_frame_time_start = undefined;
var last_frame_time_end = undefined;
var stack = [];
for (var i = 0; i < data.length; ++i)
{
if (data[i][0] == ITEM_EVENT && data[i][2] == '__framestart')
{
if (last_frame_time_end)
frames.push({'t0': last_frame_time_start, 't1': last_frame_time_end});
last_frame_time_start = data[i][1];
}
if (data[i][0] == ITEM_ENTER)
stack.push(data[i][2]);
if (data[i][0] == ITEM_LEAVE)
{
if (stack[stack.length-1] == 'frame')
last_frame_time_end = data[i][1];
stack.pop();
}
}
if(!range)
{
range = { "tmin" : frames[0].t0, "tmax" : frames[frames.length-1].t1 };
}
if (range.numframes)
{
for (var i = data.length - 1; i > 0; --i)
{
if (data[i][0] == ITEM_EVENT && data[i][2] == '__framestart')
{
end = i;
break;
}
}
var framesfound = 0;
for (var i = end - 1; i > 0; --i)
{
if (data[i][0] == ITEM_EVENT && data[i][2] == '__framestart')
{
start = i;
if (++framesfound == range.numframes)
break;
}
}
tmin = data[start][1];
tmax = data[end][1];
}
else if (range.seconds)
{
var end = data.length - 1;
for (var i = end; i > 0; --i)
{
var type = data[i][0];
if (type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE)
{
tmax = data[i][1];
break;
}
}
tmin = tmax - range.seconds;
for (var i = end; i > 0; --i)
{
var type = data[i][0];
if ((type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE) && data[i][1] < tmin)
break;
start = i;
}
}
else
{
start = 0;
end = data.length - 1;
tmin = range.tmin;
tmax = range.tmax;
for (var i = data.length-1; i > 0; --i)
{
var type = data[i][0];
if ((type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE) && data[i][1] < tmax)
{
end = i;
break;
}
}
for (var i = end; i > 0; --i)
{
var type = data[i][0];
if ((type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE) && data[i][1] < tmin)
break;
start = i;
}
// Move the start/end outwards by another frame, so we don't lose data at the edges
while (start > 0)
{
--start;
if (data[start][0] == ITEM_EVENT && data[start][2] == '__framestart')
break;
}
while (end < data.length-1)
{
++end;
if (data[end][0] == ITEM_EVENT && data[end][2] == '__framestart')
break;
}
}
var num_colours = 0;
var events = [];
// Read events for the entire data period (not just start..end)
var lastWasEvent = false;
for (var i = 0; i < data.length; ++i)
{
if (data[i][0] == ITEM_EVENT)
{
events.push({'t': data[i][1], 'id': data[i][2]});
lastWasEvent = true;
}
else if (data[i][0] == ITEM_ATTRIBUTE)
{
if (lastWasEvent)
{
if (!events[events.length-1].attrs)
events[events.length-1].attrs = [];
events[events.length-1].attrs.push(data[i][1]);
}
}
else
{
lastWasEvent = false;
}
}
var intervals = [];
var intervals_by_type = {};
// Read intervals from the focused data period (start..end)
stack = [];
var lastT = 0;
var lastWasEvent = false;
console.log(start + ","+end + " -- " + tmin+","+tmax)
console.log(start + ","+end + " -- " + range.tmin+","+range.tmax)
for (var i = start; i <= end; ++i)
{
if (data[i][0] == ITEM_EVENT)
{
// if (data[i][1] < lastT)
// console.log('Time went backwards: ' + (data[i][1] - lastT));
lastT = data[i][1];
lastWasEvent = true;
}
else if (data[i][0] == ITEM_ENTER)
{
// if (data[i][1] < lastT)
// console.log('Time - ENTER went backwards: ' + (data[i][1] - lastT) + " - " + JSON.stringify(data[i]));
stack.push({'t0': data[i][1], 'id': data[i][2]});
lastT = data[i][1];
lastWasEvent = false;
}
else if (data[i][0] == ITEM_LEAVE)
{
// if (data[i][1] < lastT)
// console.log('Time - LEAVE went backwards: ' + (data[i][1] - lastT) + " - " + JSON.stringify(data[i]));
lastT = data[i][1];
lastWasEvent = false;
if (!stack.length)
continue;
var interval = stack.pop();
if (!g_used_colours[interval.id])
g_used_colours[interval.id] = new_colour(num_colours++);
interval.colour = g_used_colours[interval.id];
interval.t1 = data[i][1];
interval.duration = interval.t1 - interval.t0;
interval.depth = stack.length;
//console.log(JSON.stringify(interval));
intervals.push(interval);
if (interval.id in intervals_by_type)
intervals_by_type[interval.id].push(intervals.length-1);
else
intervals_by_type[interval.id] = [intervals.length-1];
if (interval.id == "Script" && interval.attrs && interval.attrs.length)
{
let curT = interval.t0;
for (let subItem in interval.attrs)
{
let sub = interval.attrs[subItem];
if (sub.search("buffer") != -1)
continue;
let newInterv = {};
newInterv.t0 = curT;
newInterv.duration = +sub.replace(/.+? ([.0-9]+)us/, "$1")/1000000;
if (!newInterv.duration)
continue;
newInterv.t1 = curT + newInterv.duration;
curT += newInterv.duration;
newInterv.id = "Script:" + sub.replace(/(.+?) ([.0-9]+)us/, "$1");
newInterv.colour = g_used_colours[interval.id];
newInterv.depth = interval.depth+1;
intervals.push(newInterv);
if (newInterv.id in intervals_by_type)
intervals_by_type[newInterv.id].push(intervals.length-1);
else
intervals_by_type[newInterv.id] = [intervals.length-1];
}
}
}
else if (data[i][0] == ITEM_ATTRIBUTE)
{
if (!lastWasEvent && stack.length)
{
if (!stack[stack.length-1].attrs)
stack[stack.length-1].attrs = [];
stack[stack.length-1].attrs.push(data[i][1]);
}
}
}
return { 'frames': frames, 'events': events, 'intervals': intervals, 'intervals_by_type' : intervals_by_type, 'tmin': tmin, 'tmax': tmax };
}
outInterface.data = function() { return g_data; };
outInterface.raw_data = function() { return g_raw_data; };
outInterface.data_by_frame = function() { return g_data_by_frame; };
refresh(callback, tryLive, file);
return outInterface;
};

View File

@ -0,0 +1,471 @@
// Copyright (c) 2016 Wildfire Games
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Handles the drawing of a report
var g_report_draw = (function()
{
var outInterface = {};
var mouse_is_down = null;
function rebuild_canvases(raw_data)
{
g_canvas = {};
g_canvas.canvas_frames = $('<canvas width="1600" height="128"></canvas>').get(0);
g_canvas.threads = {};
for (var thread = 0; thread < raw_data.threads.length; thread++)
g_canvas.threads[thread] = $('<canvas width="1600" height="128"></canvas>').get(0);
g_canvas.canvas_zoom = $('<canvas width="1600" height="192"></canvas>').get(0);
g_canvas.text_output = $('<pre></pre>').get(0);
$('#timelines').empty();
$('#timelines').append(g_canvas.canvas_frames);
for (var thread = 0; thread < raw_data.threads.length; thread++)
$('#timelines').append($(g_canvas.threads[thread]));
$('#timelines').append(g_canvas.canvas_zoom);
$('#timelines').append(g_canvas.text_output);
}
outInterface.rebuild_canvases = rebuild_canvases;
function update_display(data, range)
{
let mainData = data.threads[g_main_thread];
if (range.seconds)
{
range.tmax = mainData.frames[mainData.frames.length-1].t1;
range.tmin = mainData.frames[mainData.frames.length-1].t1-range.seconds;
}
else if (range.frames)
{
range.tmax = mainData.frames[mainData.frames.length-1].t1;
range.tmin = mainData.frames[mainData.frames.length-1-range.frames].t0;
}
$(g_canvas.text_output).empty();
display_frames(data.threads[g_main_thread], g_canvas.canvas_frames, range);
display_events(data.threads[g_main_thread], g_canvas.canvas_frames, range);
set_frames_zoom_handlers(data, g_canvas.canvas_frames);
set_tooltip_handlers(g_canvas.canvas_frames);
$(g_canvas.canvas_zoom).unbind();
set_zoom_handlers(data.threads[g_main_thread], data.threads[g_main_thread], g_canvas.threads[g_main_thread], g_canvas.canvas_zoom);
set_tooltip_handlers(data.canvas_zoom);
for (var i = 0; i < data.threads.length; i++)
{
$(g_canvas.threads[i]).unbind();
let events = slice_intervals(data.threads[i], range);
display_hierarchy(data.threads[i], events, g_canvas.threads[i], {});
set_zoom_handlers(data.threads[i], events, g_canvas.threads[i], g_canvas.canvas_zoom);
set_tooltip_handlers(g_canvas.threads[i]);
};
}
outInterface.update_display = update_display;
function display_frames(data, canvas, range)
{
canvas._tooltips = [];
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
var xpadding = 8;
var padding_top = 40;
var width = canvas.width - xpadding*2;
var height = canvas.height - padding_top - 4;
var tmin = data.tmin;
var tmax = data.tmax;
var dx = width / (tmax-tmin);
canvas._zoomData = {
'x_to_t': x => tmin + (x - xpadding) / dx,
't_to_x': t => (t - tmin) * dx + xpadding
};
// log 100 scale, skip < 15 ms (60fps)
var scale = x => 1 - Math.max(0, Math.log(1 + (x-15)/10) / Math.log(100));
ctx.strokeStyle = 'rgb(0, 0, 0)';
ctx.fillStyle = 'rgb(255, 255, 255)';
for (var i = 0; i < data.frames.length; ++i)
{
var frame = data.frames[i];
var duration = frame.t1 - frame.t0;
var x0 = xpadding + dx*(frame.t0 - tmin);
var x1 = x0 + dx*duration;
var y1 = canvas.height;
var y0 = y1 * scale(duration*1000);
ctx.beginPath();
ctx.rect(x0, y0, x1-x0, y1-y0);
ctx.stroke();
canvas._tooltips.push({
'x0': x0, 'x1': x1,
'y0': y0, 'y1': y1,
'text': function(frame, duration) { return function() {
var t = '<b>Frame</b><br>';
t += 'Length: ' + time_label(duration) + '<br>';
if (frame.attrs)
{
frame.attrs.forEach(function(attr)
{
t += attr + '<br>';
});
}
return t;
}} (frame, duration)
});
}
[16, 33, 200, 500].forEach(function(t)
{
var y1 = canvas.height;
var y0 = y1 * scale(t);
var y = Math.floor(y0) + 0.5;
ctx.beginPath();
ctx.moveTo(xpadding, y);
ctx.lineTo(canvas.width - xpadding, y);
ctx.strokeStyle = 'rgb(255, 0, 0)';
ctx.stroke();
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillText(t+'ms', 0, y-2);
});
ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)';
ctx.fillStyle = 'rgba(128, 128, 255, 0.2)';
ctx.beginPath();
ctx.rect(xpadding + dx*(range.tmin - tmin), 0, dx*(range.tmax - range.tmin), canvas.height);
ctx.fill();
ctx.stroke();
ctx.restore();
}
outInterface.display_frames = display_frames;
function display_events(data, canvas)
{
var ctx = canvas.getContext('2d');
ctx.save();
var x_to_time = canvas._zoomData.x_to_t;
var time_to_x = canvas._zoomData.t_to_x;
for (var i = 0; i < data.events.length; ++i)
{
var event = data.events[i];
if (event.id == '__framestart')
continue;
if (event.id == 'gui event' && event.attrs && event.attrs[0] == 'type: mousemove')
continue;
var x = time_to_x(event.t);
var y = 32;
if (x < 2)
continue;
var x0 = x;
var x1 = x;
var y0 = y-4;
var y1 = y+4;
ctx.strokeStyle = 'rgb(255, 0, 0)';
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.stroke();
canvas._tooltips.push({
'x0': x0, 'x1': x1,
'y0': y0, 'y1': y1,
'text': function(event) { return function() {
var t = '<b>' + event.id + '</b><br>';
if (event.attrs)
{
event.attrs.forEach(function(attr) {
t += attr + '<br>';
});
}
return t;
}} (event)
});
}
ctx.restore();
}
outInterface.display_events = display_events;
function display_hierarchy(main_data, data, canvas, range, zoom)
{
canvas._tooltips = [];
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.font = '12px sans-serif';
var xpadding = 8;
var padding_top = 40;
var width = canvas.width - xpadding*2;
var height = canvas.height - padding_top - 4;
var tmin, tmax, start, end;
if (range.tmin)
{
tmin = range.tmin;
tmax = range.tmax;
}
else
{
tmin = data.tmin;
tmax = data.tmax;
}
canvas._hierarchyData = { 'range': range, 'tmin': tmin, 'tmax': tmax };
function time_to_x(t)
{
return xpadding + (t - tmin) / (tmax - tmin) * width;
}
function x_to_time(x)
{
return tmin + (x - xpadding) * (tmax - tmin) / width;
}
ctx.save();
ctx.textAlign = 'center';
ctx.strokeStyle = 'rgb(192, 192, 192)';
ctx.beginPath();
var precision = -3;
while ((tmax-tmin)*Math.pow(10, 3+precision) < 25)
++precision;
if (precision > 10)
precision = 10;
if (precision < 0)
precision = 0;
var ticks_per_sec = Math.pow(10, 3+precision);
var major_tick_interval = 5;
for (var i = 0; i < (tmax-tmin)*ticks_per_sec; ++i)
{
var major = (i % major_tick_interval == 0);
var x = Math.floor(time_to_x(tmin + i/ticks_per_sec));
ctx.moveTo(x-0.5, padding_top - (major ? 4 : 2));
ctx.lineTo(x-0.5, padding_top + height);
if (major)
ctx.fillText((i*1000/ticks_per_sec).toFixed(precision), x, padding_top - 8);
}
ctx.stroke();
ctx.restore();
var BAR_SPACING = 16;
for (var i = 0; i < data.intervals.length; ++i)
{
var interval = data.intervals[i];
if (interval.tmax <= tmin || interval.tmin > tmax)
continue;
var x0 = Math.floor(time_to_x(interval.t0));
var x1 = Math.floor(time_to_x(interval.t1));
if (x1-x0 < 1)
continue;
var y0 = padding_top + interval.depth * BAR_SPACING;
var y1 = y0 + BAR_SPACING;
var label = interval.id;
if (interval.attrs)
{
if (/^\d+$/.exec(interval.attrs[0]))
label += ' ' + interval.attrs[0];
else
label += ' [...]';
}
ctx.fillStyle = interval.colour;
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.rect(x0-0.5, y0-0.5, x1-x0, y1-y0);
ctx.fill();
ctx.stroke();
ctx.fillStyle = 'black';
ctx.fillText(label, x0+2, y0+BAR_SPACING-4, Math.max(1, x1-x0-4));
canvas._tooltips.push({
'x0': x0, 'x1': x1,
'y0': y0, 'y1': y1,
'text': function(interval) { return function() {
var t = '<b>' + interval.id + '</b><br>';
t += 'Length: ' + time_label(interval.duration) + '<br>';
if (interval.attrs)
{
interval.attrs.forEach(function(attr) {
t += attr + '<br>';
});
}
return t;
}} (interval)
});
}
for (var i = 0; i < main_data.frames.length; ++i)
{
var frame = main_data.frames[i];
if (frame.t0 < tmin || frame.t0 > tmax)
continue;
var x = Math.floor(time_to_x(frame.t0));
ctx.save();
ctx.lineWidth = 3;
ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)';
ctx.beginPath();
ctx.moveTo(x+0.5, 0);
ctx.lineTo(x+0.5, canvas.height);
ctx.stroke();
ctx.fillText(((frame.t1 - frame.t0) * 1000).toFixed(0)+'ms', x+2, padding_top - 24);
ctx.restore();
}
if (zoom)
{
var x0 = time_to_x(zoom.tmin);
var x1 = time_to_x(zoom.tmax);
ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)';
ctx.fillStyle = 'rgba(128, 128, 255, 0.2)';
ctx.beginPath();
ctx.moveTo(x0+0.5, 0.5);
ctx.lineTo(x1+0.5, 0.5);
ctx.lineTo(x1+0.5 + 4, canvas.height-0.5);
ctx.lineTo(x0+0.5 - 4, canvas.height-0.5);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
ctx.restore();
}
outInterface.display_hierarchy = display_hierarchy;
function set_frames_zoom_handlers(data, canvas0)
{
function do_zoom(data, event)
{
var zdata = canvas0._zoomData;
var relativeX = event.pageX - this.offsetLeft;
var relativeY = event.pageY - this.offsetTop;
var width = relativeY / canvas0.height;
width = width*width;
width *= zdata.x_to_t(canvas0.width)/10;
var tavg = zdata.x_to_t(relativeX);
var tmax = tavg + width/2;
var tmin = tavg - width/2;
var range = {'tmin': tmin, 'tmax': tmax};
update_display(data, range);
}
$(canvas0).unbind();
$(canvas0).mousedown(function(event)
{
mouse_is_down = canvas0;
do_zoom.call(this, data, event);
});
$(canvas0).mouseup(function(event)
{
mouse_is_down = null;
});
$(canvas0).mousemove(function(event)
{
if (mouse_is_down)
do_zoom.call(this, data, event);
});
}
function set_zoom_handlers(main_data, data, canvas0, canvas1)
{
function do_zoom(event)
{
var hdata = canvas0._hierarchyData;
function x_to_time(x)
{
return hdata.tmin + x * (hdata.tmax - hdata.tmin) / canvas0.width;
}
var relativeX = event.pageX - this.offsetLeft;
var relativeY = (event.pageY + this.offsetTop) / canvas0.height;
relativeY = relativeY - 0.5;
relativeY *= 5;
relativeY *= relativeY;
var width = relativeY / canvas0.height;
width = width*width;
width = 3 + width * x_to_time(canvas0.width)/10;
var zoom = { tmin: x_to_time(relativeX-width/2), tmax: x_to_time(relativeX+width/2) };
display_hierarchy(main_data, data, canvas0, hdata.range, zoom);
display_hierarchy(main_data, data, canvas1, zoom, undefined);
set_tooltip_handlers(canvas1);
}
$(canvas0).mousedown(function(event)
{
mouse_is_down = canvas0;
do_zoom.call(this, event);
});
$(canvas0).mouseup(function(event)
{
mouse_is_down = null;
});
$(canvas0).mousemove(function(event)
{
if (mouse_is_down)
do_zoom.call(this, event);
});
}
return outInterface;
})();

View File

@ -1,28 +1,83 @@
<!DOCTYPE html>
<head>
<title>0 A.D. profiler UI</title>
<script src="jquery-1.6.4.js"></script>
<script src="utilities.js"></script>
<script src="ReportDraw.js"></script>
<script src="Profiler2Report.js"></script>
<script src="profiler2.js"></script>
<script>
$(refresh);
</script>
<style>
canvas { border: 1px #ddd solid; }
@keyframes rotate
{
0% {transform:translateY(-50%) rotate(0deg);}
100% {transform:translateY(-50%) rotate(360deg);}
}
html { font-size: 14px; font-family: "Source Code Pro", monospace; padding:10px;}
* { box-sizing:border-box; margin:0; padding: 0; }
canvas { border: 1px #ddd solid; display:block;}
header h1 { font-size:2em; }
header nav { height:50px; margin:5px; border:1px solid gray; }
header nav p { display:inline-block; height:100%; padding: 15px 10px; background:#aaa; cursor:pointer; border-right:1px solid gray; position:relative;}
header nav p.loading { background:#ccf; cursor:progress; padding-right:15px; }
header nav p.loading:after { content:""; position:absolute; right:5px; top:50%; transform:translateY(-50%); height:5px;width:5px;background:#ccc;border:1px solid gray; animation:rotate 2s infinite; }
header nav p.fail { background:#faa;cursor:not-allowed;}
header nav p.active { cursor:pointer; background:#eee; box-shadow: 0px 0px 2px 0px rgba(0,0,0,0.5);font-weight:bold;}
#tooltip { background: #ffd; padding: 4px; font: 12px sans-serif; border: 1px #880 solid; }
#tooltip.long { -moz-column-count: 2; }
#choices { display: flex; flex-wrap:wrap;}
#choices section { position:relative;}
#choices section h2 { height:25px; }
#choices section aside { width:200px; position:absolute; top: 26px; right : 1px; border-left:1px solid gray;border-bottom:1px solid gray; background:rgba(255,255,255,0.5);}
#choices section aside:hover { opacity:0.2; }
#choices section aside p { margin:4px 0px 4px 2px; border-left:15px solid; padding-left: 5px; font-size:0.8rem; }
#choices nav { flex-shrink:0; width:300px; height:600px; overflow-y: scroll;}
#choices nav p { margin:2px; cursor:pointer;}
#choices nav p:hover { background:#eee;}
#choices nav p.active { background:#fee;}
#comparison { min-width:400px; }
#comparison table { border-collapse:collapse; margin: 20px 10px;}
#comparison td,#comparison th { min-width:80px;height:25px;text-align: center;}
#comparison th { border: 1px solid #888; background:#eee; }
#comparison td { border: 1px solid #bbb; }
</style>
</head>
<body>
<button onclick="save_as_file()">Save Live Report to file</button>
<button onclick="refresh()">Refresh</button>
<br>
<div id="timelines"></div>
<header>
<h1>Open reports</h1>
<p>Use the input field below to load a new report (from JSON)</p>
<input type="file" id="report_load_input" name="files[]" />
<nav></nav>
</header>
<p>Click on the following timelines to zoom.</p>
</a><div id="timelines"></div>
<div id="choices">
<div style="width:100%">
<h1>Analysis</h1>
<p>Click on any of the event names in "choices" to see more details about them. Load more reports to compare.</p>
</div>
<section id="frequency_graph">
<h2>Frequency Graph</h2>
<aside></aside>
<canvas id="canvas_frequency" width="600" height="600"></canvas>
</section>
<section id="history_graph">
<h2>Frame-by-Frame Graph</h2>
<aside></aside>
<div style="width:602px;overflow:auto;"><canvas id="canvas_history" width="600" height="600"></canvas></div>
</section>
<nav>
<h3>Choices</h3>
</nav>
<div id="comparison">
<h3>Report Comparison</h3>
</div>
</div>
<div id="tooltip" style="position: absolute; visibility: hidden"></div>
Search: <input id="regionsearch" oninput="search_regions(this.value)">
<table id="regionsearchresult">
<thead>
<tr><th>Frame<th>Region<th>Time (msec)
</thead>
<tbody>
</table>
<pre id="debug"></pre>
<pre id="debug"></pre>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,189 @@
// Copyright (c) 2016 Wildfire Games
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// Various functions used by several of the tiles.
function hslToRgb(h, s, l, a)
{
var r, g, b;
if (s == 0)
{
r = g = b = l;
}
else
{
function hue2rgb(p, q, t)
{
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return 'rgba(' + Math.floor(r * 255) + ',' + Math.floor(g * 255) + ',' + Math.floor(b * 255) + ',' + a + ')';
}
function new_colour(id)
{
var hs = [0, 1/3, 2/3, 1/4, 2/4, 3/4, 1/5, 3/5, 2/5, 4/5];
var ss = [1, 0.5];
var ls = [0.8, 0.6, 0.9, 0.7];
return hslToRgb(hs[id % hs.length], ss[Math.floor(id / hs.length) % ss.length], ls[Math.floor(id / (hs.length*ss.length)) % ls.length], 1);
}
function graph_colour(id)
{
var hs = [0, 1/3, 2/3, 2/4, 3/4, 1/5, 3/5, 2/5, 4/5];
return hslToRgb(hs[id % hs.length], 0.7, 0.5, 1);
}
function concat_events(data)
{
var events = [];
data.events.forEach(function(ev) {
ev.pop(); // remove the dummy null markers
Array.prototype.push.apply(events, ev);
});
return events;
}
function time_label(t, precision = 2)
{
if (t < 0)
return "-" + time_label(-t, precision);
if (t > 1e-3)
return (t * 1e3).toFixed(precision) + 'ms';
else
return (t * 1e6).toFixed(precision) + 'us';
}
function slice_intervals(data, range)
{
var tmin = 0;
var tmax = 0;
if (range.seconds)
{
tmax = data.frames[data.frames.length-1].t1;
tmin = data.frames[data.frames.length-1].t1-range.seconds;
}
else if (range.frames)
{
tmax = data.frames[data.frames.length-1].t1;
tmin = data.frames[data.frames.length-1-range.frames].t0;
}
else
{
tmax = range.tmax;
tmin = range.tmin;
}
var events = { "tmin" : tmin, "tmax" : tmax, "intervals" : [] };
for (let itv in data.intervals)
{
let interval = data.intervals[itv];
if (interval.t0 > tmin && interval.t0 < tmax)
events.intervals.push(interval);
}
return events;
}
function smooth_1D(array, i, distance)
{
let value = 0;
let total = 0;
for (let j = i - distance; j <= i + distance; j++)
{
value += array[j]*(1+distance*distance - (j-i)*(j-i) );
total += (1+distance*distance - (j-i)*(j-i) );
}
return value/total;
}
function smooth_1D_array(array, distance)
{
let copied = array.slice(0);
for (let i =0; i < array.length; ++i)
{
let value = 0;
let total = 0;
for (let j = i - distance; j <= i + distance; j++)
{
value += array[j]*(1+distance*distance - (j-i)*(j-i) );
total += (1+distance*distance - (j-i)*(j-i) );
}
copied[i] = value/total;
}
return copied;
}
function set_tooltip_handlers(canvas)
{
function do_tooltip(event)
{
var tooltips = canvas._tooltips;
if (!tooltips)
return;
var relativeX = event.pageX - this.getBoundingClientRect().left - window.scrollX;
var relativeY = event.pageY - this.getBoundingClientRect().top - window.scrollY;
var text = undefined;
for (var i = 0; i < tooltips.length; ++i)
{
var t = tooltips[i];
if (t.x0-1 <= relativeX && relativeX <= t.x1+1 && t.y0 <= relativeY && relativeY <= t.y1)
{
text = t.text();
break;
}
}
if (text)
{
if (text.length > 512)
$('#tooltip').addClass('long');
else
$('#tooltip').removeClass('long');
$('#tooltip').css('left', (event.pageX+16)+'px');
$('#tooltip').css('top', (event.pageY+8)+'px');
$('#tooltip').html(text);
$('#tooltip').css('visibility', 'visible');
}
else
{
$('#tooltip').css('visibility', 'hidden');
}
}
$(canvas).mousemove(function(event) {
do_tooltip.call(this, event);
});
$(canvas).mouseleave(function(event) {
$('#tooltip').css('visibility', 'hidden');
});
}