diff --git a/source/ps/Profiler2.cpp b/source/ps/Profiler2.cpp index 5f8a50599e..a8e54febc2 100644 --- a/source/ps/Profiler2.cpp +++ b/source/ps/Profiler2.cpp @@ -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 +#include 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 topLevelArgs; + + typedef std::tuple > infoPerType; + std::unordered_map timeByType; + std::vector last_time_stack; + std::vector last_names; + + // never too many hacks + std::string current_attribute = ""; + std::map 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); +} diff --git a/source/ps/Profiler2.h b/source/ps/Profiler2.h index a1228b379b..8299ab1a94 100644 --- a/source/ps/Profiler2.h +++ b/source/ps/Profiler2.h @@ -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) /** diff --git a/source/ps/UserReport.cpp b/source/ps/UserReport.cpp index ea1f7be1bb..2d553a03fc 100644 --- a/source/ps/UserReport.cpp +++ b/source/ps/UserReport.cpp @@ -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() diff --git a/source/scriptinterface/ScriptInterface.cpp b/source/scriptinterface/ScriptInterface.cpp index 12b8b55760..7a039ffea3 100644 --- a/source/scriptinterface/ScriptInterface.cpp +++ b/source/scriptinterface/ScriptInterface.cpp @@ -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; diff --git a/source/scriptinterface/ScriptRuntime.cpp b/source/scriptinterface/ScriptRuntime.cpp index b51c2dfa70..1d63f1d5cd 100644 --- a/source/scriptinterface/ScriptRuntime.cpp +++ b/source/scriptinterface/ScriptRuntime.cpp @@ -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 diff --git a/source/tools/profiler2/Profiler2Report.js b/source/tools/profiler2/Profiler2Report.js new file mode 100644 index 0000000000..72992fba52 --- /dev/null +++ b/source/tools/profiler2/Profiler2Report.js @@ -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 = $(''); + 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; +}; diff --git a/source/tools/profiler2/ReportDraw.js b/source/tools/profiler2/ReportDraw.js new file mode 100644 index 0000000000..dbefd8fc6d --- /dev/null +++ b/source/tools/profiler2/ReportDraw.js @@ -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 = $('').get(0); + g_canvas.threads = {}; + + for (var thread = 0; thread < raw_data.threads.length; thread++) + g_canvas.threads[thread] = $('').get(0); + + g_canvas.canvas_zoom = $('').get(0); + g_canvas.text_output = $('
').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 = 'Frame
'; + t += 'Length: ' + time_label(duration) + '
'; + if (frame.attrs) + { + frame.attrs.forEach(function(attr) + { + t += attr + '
'; + }); + } + 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 = '' + event.id + '
'; + if (event.attrs) + { + event.attrs.forEach(function(attr) { + t += attr + '
'; + }); + } + 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 = '' + interval.id + '
'; + t += 'Length: ' + time_label(interval.duration) + '
'; + if (interval.attrs) + { + interval.attrs.forEach(function(attr) { + t += attr + '
'; + }); + } + 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; +})(); \ No newline at end of file diff --git a/source/tools/profiler2/profiler2.html b/source/tools/profiler2/profiler2.html index 3ba941688c..8657fdcbb2 100644 --- a/source/tools/profiler2/profiler2.html +++ b/source/tools/profiler2/profiler2.html @@ -1,28 +1,83 @@ + 0 A.D. profiler UI + + + - + + + - -
-
+
+

Open reports

+

Use the input field below to load a new report (from JSON)

+ + +
+

Click on the following timelines to zoom.

+
+ +
+
+

Analysis

+

Click on any of the event names in "choices" to see more details about them. Load more reports to compare.

+
+
+

Frequency Graph

+ + +
+
+

Frame-by-Frame Graph

+ +
+
+ +
+

Report Comparison

+
+
-Search: - - - - -
FrameRegionTime (msec) -
- -

\ No newline at end of file
+

+
+
\ No newline at end of file
diff --git a/source/tools/profiler2/profiler2.js b/source/tools/profiler2/profiler2.js
index 341ac64124..02f8f392f7 100644
--- a/source/tools/profiler2/profiler2.js
+++ b/source/tools/profiler2/profiler2.js
@@ -1,849 +1,497 @@
-// TODO: this code needs a load of cleaning up and documenting,
-// and feature additions and general improvement and unrubbishing
+// 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.
 
-// Item types returned by the engine
-var ITEM_EVENT = 1;
-var ITEM_ENTER = 2;
-var ITEM_LEAVE = 3;
-var ITEM_ATTRIBUTE = 4;
+// This file is the main handler, which deals with loading reports and showing the analysis graphs
+// the latter could probably be put in a separate module
 
+// global array of Profiler2Report objects
+var g_reports = [];
 
-function hslToRgb(h, s, l, a)
-{
-    var r, g, b;
+var g_main_thread = 0;
+var g_current_report = 0;
 
-    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 g_profile_path = null;
+var g_active_elements = [];
+var g_loading_timeout = null;
 
-        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);
-}
-
-var g_used_colours = {};
-
-
-var g_data;
-
-function refresh()
-{
-    if (1)
-        refresh_live();
-    else
-        refresh_jsonp('../../../binaries/system/profile2.jsonp');
-}
-
-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 refresh_jsonp(url)
-{
-    var script = document.createElement('script');
-    
-    window.profileDataCB = function(data)
-    {
-        script.parentNode.removeChild(script);
-
-        var threads = [];
-        data.threads.forEach(function(thread) {
-            var canvas = $('');
-            threads.push({'name': thread.name, 'data': { 'events': concat_events(thread.data) }, 'canvas': canvas.get(0)});
-        });
-        g_data = { 'threads': threads };
-
-        var range = {'seconds': 0.05};
-
-        rebuild_canvases();
-        update_display(range);
-    };
-
-    script.src = url;
-    document.body.appendChild(script);
-}
-
-function refresh_live()
+function save_as_file()
 {
     $.ajax({
-        url: 'http://127.0.0.1:8000/overview',
-        dataType: 'json',
-        success: function (data) {
-            var threads = [];
-            data.threads.forEach(function(thread) {
-                var canvas = $('');
-                threads.push({'name': thread.name, 'canvas': canvas.get(0)});
-            });
-            var callback_data = { 'threads': threads, 'completed': 0 };
-            threads.forEach(function(thread) {
-                refresh_thread(thread, callback_data);
-            });
+        url: 'http://127.0.0.1:8000/download',
+        success: function () {
         },
         error: function (jqXHR, textStatus, errorThrown) {
-            alert('Failed to connect to server ("'+textStatus+'")');
         }
     });
 }
 
-function refresh_thread(thread, callback_data)
+function get_history_data(report, thread, type)
 {
-    $.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_data = { 'threads': callback_data.threads };
+    var ret = {"time_by_frame":[], "max" : 0, "log_scale" : null};
+ 
+    var report_data = g_reports[report].data().threads[thread];
+    var interval_data = report_data.intervals;
 
-                //var range = {'numframes': 5};
-                var range = {'seconds': 0.05};
+    let data = report_data.intervals_by_type_frame[type];
+    if (!data)
+        return ret;
 
-                rebuild_canvases();
-                update_display(range);
-            }
-        },
-        error: function (jqXHR, textStatus, errorThrown) {
-            alert('Failed to connect to server ("'+textStatus+'")');
-        }
-    });
+    let max = 0;
+    let avg = [];
+    let current_frame = 0;
+    for (let i = 0; i < data.length; i++)
+    {
+        ret.time_by_frame.push(0);
+        for (let p = 0; p < data[i].length; p++)
+            ret.time_by_frame[ret.time_by_frame.length-1] += interval_data[data[i][p]].duration;
+    }
+
+    // somehow JS sorts 0.03 lower than 3e-7 otherwise
+    let sorted = ret.time_by_frame.slice(0).sort((a,b) => a-b);
+    ret.max = sorted[sorted.length-1];
+    avg = sorted[Math.round(avg.length/2)];
+
+    if (ret.max > avg * 3)
+        ret.log_scale = true;
+
+    return ret;
 }
 
-function rebuild_canvases()
-{
-    g_data.canvas_frames = $('').get(0);
-    g_data.canvas_zoom = $('').get(0);
-    g_data.text_output = $('
').get(0);
-    
-    set_frames_zoom_handlers(g_data.canvas_frames);
-    set_tooltip_handlers(g_data.canvas_frames);
-
-    $('#timelines').empty();
-    $('#timelines').append(g_data.canvas_frames);
-    g_data.threads.forEach(function(thread) {
-        $('#timelines').append($(thread.canvas));
-    });
-    $('#timelines').append(g_data.canvas_zoom);
-    $('#timelines').append(g_data.text_output);
-}
-
-function update_display(range)
-{
-    $(g_data.text_output).empty();
-
-    var main_events = g_data.threads[0].data.events;
-
-    var processed_main = g_data.threads[0].processed_events = compute_intervals(main_events, range);
-
-//    display_top_items(main_events, g_data.text_output);
-
-    display_frames(processed_main, g_data.canvas_frames);
-    display_events(processed_main, g_data.canvas_frames);
-
-    $(g_data.threads[0].canvas).unbind();
-    $(g_data.canvas_zoom).unbind();
-    display_hierarchy(processed_main, processed_main, g_data.threads[0].canvas, {}, undefined);
-    set_zoom_handlers(processed_main, processed_main, g_data.threads[0].canvas, g_data.canvas_zoom);
-    set_tooltip_handlers(g_data.threads[0].canvas);
-    set_tooltip_handlers(g_data.canvas_zoom);
-
-    g_data.threads.slice(1).forEach(function(thread) {
-        var processed_data = compute_intervals(thread.data.events, {'tmin': processed_main.tmin, 'tmax': processed_main.tmax});
-
-        $(thread.canvas).unbind();
-        display_hierarchy(processed_main, processed_data, thread.canvas, {}, undefined);
-        set_zoom_handlers(processed_main, processed_data, thread.canvas, g_data.canvas_zoom);
-        set_tooltip_handlers(thread.canvas);
-    });
- }
-
-function display_top_items(data, output)
-{
-    var items = {};
-    for (var i = 0; i < data.length; ++i)
-    {
-        var type = data[i][0];
-        if (!(type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE))
-            continue;
-        var id = data[i][2];
-        if (!items[id])
-            items[id] = { 'count': 0 };
-        items[id].count++;
-    }
-
-    var topitems = [];
-    for (var k in items)
-        topitems.push([k, items[k].count]);
-    topitems.sort(function(a, b) {
-        return b[1] - a[1];
-    });
-    topitems.splice(16);
-    
-    topitems.forEach(function(item) {
-        output.appendChild(document.createTextNode(item[1] + 'x -- ' + item[0] + '\n'));
-    });
-    output.appendChild(document.createTextNode('(' + data.length + ' items)'));
-}
-
-function compute_intervals(data, range)
-{
-    var start, end;
-    var tmin, tmax;
-    
-    var frames = [];
-    var last_frame_time = undefined;
-    for (var i = 0; i < data.length; ++i)
-    {
-        if (data[i][0] == ITEM_EVENT && data[i][2] == '__framestart')
-        {
-            var t = data[i][1];
-            if (last_frame_time)
-                frames.push({'t0': last_frame_time, 't1': t});
-            last_frame_time = t;
-        }
-    }
-    
-    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 = [];
-    
-    // Read intervals from the focused data period (start..end)
-    var stack = [];
-    var lastT = 0;
-    var lastWasEvent = false;
-    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 went backwards: ' + (data[i][1] - lastT));
-
-            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 went backwards: ' + (data[i][1] - lastT));
-
-            lastT = data[i][1];
-            lastWasEvent = false;
-            
-            if (!stack.length)
-                continue;
-            var interval = stack.pop();
-
-            if (data[i][2] != interval.id && data[i][2] != '(ProfileStop)')
-                alert('inconsistent interval ids ('+interval.id+' / '+data[i][2]+')');
-
-            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;
-            
-            intervals.push(interval);
-        }
-        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, 'tmin': tmin, 'tmax': tmax };
-}
-
-function time_label(t)
-{
-    if (t > 1e-3)
-        return (t * 1e3).toFixed(2) + 'ms';
-    else
-        return (t * 1e6).toFixed(2) + 'us';
-}
-
-function display_frames(data, canvas)
+function draw_frequency_graph()
 {
+    let canvas = document.getElementById("canvas_frequency");
     canvas._tooltips = [];
 
-    var ctx = canvas.getContext('2d');
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-    ctx.save();
+    let context = canvas.getContext("2d");
+    context.clearRect(0, 0, canvas.width, canvas.height);
 
-    var xpadding = 8;
-    var padding_top = 40;
-    var width = canvas.width - xpadding*2;
-    var height = canvas.height - padding_top - 4;
+    let legend = document.getElementById("frequency_graph").querySelector("aside");
+    legend.innerHTML = "";
 
-    var tmin = data.frames[0].t0;
-    var tmax = data.frames[data.frames.length-1].t1;
-    var dx = width / (tmax-tmin);
-    
-    canvas._zoomData = {
-        'x_to_t': function(x) {
-            return tmin + (x - xpadding) / dx;
-        },
-        't_to_x': function(t) {
-            return (t - tmin) * dx + xpadding;
+    if (!g_active_elements.length)
+        return;
+
+    var series_data = {};
+    var use_log_scale = null;
+
+    var x_scale = 0;
+    var y_scale = 0;
+    var padding = 10;
+
+    var item_nb = 0;
+
+    var tooltip_helper = {};
+
+    for (let typeI in g_active_elements)
+    {
+        for (let rep in g_reports)
+        {
+            item_nb++;
+            let data = get_history_data(rep, g_main_thread, g_active_elements[typeI]);
+            let name = rep + "/" + g_active_elements[typeI];
+            series_data[name] = data.time_by_frame.filter(a=>a).sort((a,b) => a-b);
+            if (series_data[name].length > x_scale)
+                x_scale = series_data[name].length;
+            if (data.max > y_scale)
+                y_scale = data.max;
+            if (use_log_scale === null && data.log_scale)
+                use_log_scale = true;
         }
-    };
-    
-//    var y_per_second = 1000;
-    var y_per_second = 100;
+    }
+    if (use_log_scale)
+    {
+        let legend_item = document.createElement("p");
+        legend_item.style.borderColor = "transparent";
+        legend_item.textContent = " -- log x scale -- ";
+        legend.appendChild(legend_item);
+    }
+    let id = 0;
+    for (let type in series_data)
+    {
+        let colour = graph_colour(id);
+        let time_by_frame = series_data[type];
+        let p = 0;
+        let last_val = 0;
 
-    [16, 33, 200, 500].forEach(function(t) {
-        var y1 = canvas.height;
-        var y0 = y1 - t/1000*y_per_second;
-        var y = Math.floor(y0) + 0.5;
+        let nb = document.createElement("p");
+        nb.style.borderColor = colour;
+        nb.textContent = type + " - n=" + time_by_frame.length;
+        legend.appendChild(nb);
 
-        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);
+        for (var i = 0; i < time_by_frame.length; i++)
+        {
+            let x0 = i/time_by_frame.length*(canvas.width-padding*2) + padding;
+            if (i == 0)
+                x0 = 0;
+            let x1 = (i+1)/time_by_frame.length*(canvas.width-padding*2) + padding;
+            if (i == time_by_frame.length-1)
+                x1 = (time_by_frame.length-1)*canvas.width;
+
+            let y = time_by_frame[i]/y_scale;
+            if (use_log_scale)
+                y = Math.log10(1 + time_by_frame[i]/y_scale * 9);
+
+            context.globalCompositeOperation = "lighter";
+
+            context.beginPath();
+            context.strokeStyle = colour
+            context.lineWidth = 0.5;
+            context.moveTo(x0,canvas.height * (1 - last_val));
+            context.lineTo(x1,canvas.height * (1 - y));
+            context.stroke();
+
+            last_val = y;
+            if (!tooltip_helper[Math.floor(x0)])
+                tooltip_helper[Math.floor(x0)] = [];
+            tooltip_helper[Math.floor(x0)].push([y, type]);
+        }
+        id++;
+    }
+
+    for (let i in tooltip_helper)
+    {
+        let tooltips = tooltip_helper[i];
+        let text = "";
+        for (let j in tooltips)
+            if (tooltips[j][0] != undefined && text.search(tooltips[j][1])===-1)
+                text += "Series " + tooltips[j][1] + ": " + time_label((tooltips[j][0])*y_scale,1) + "
"; + canvas._tooltips.push({ + 'x0': +i, 'x1': +i+1, + 'y0': 0, 'y1': canvas.height, + 'text': function(text) { return function() { return text; } }(text) + }); + } + set_tooltip_handlers(canvas); + + [0.02,0.05,0.1,0.25,0.5,0.75].forEach(function(y_val) + { + let y = y_val; + if (use_log_scale) + y = Math.log10(1 + y_val * 9); + + context.beginPath(); + context.lineWidth="1"; + context.strokeStyle = "rgba(0,0,0,0.2)"; + context.moveTo(0,canvas.height * (1- y)); + context.lineTo(canvas.width,canvas.height * (1 - y)); + context.stroke(); + context.fillStyle = "gray"; + context.font = "10px Arial"; + context.textAlign="left"; + context.fillText(time_label(y*y_scale,0), 2, canvas.height * (1 - y) - 2 ); }); - - 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 - duration*y_per_second; - - 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 = 'Frame
'; - t += 'Length: ' + time_label(duration) + '
'; - if (frame.attrs) - { - frame.attrs.forEach(function(attr) { - t += attr + '
'; - }); - } - return t; - }} (frame, duration) - }); - } - - ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)'; - ctx.fillStyle = 'rgba(128, 128, 255, 0.2)'; - ctx.beginPath(); - ctx.rect(xpadding + dx*(data.tmin - tmin), 0, dx*(data.tmax - data.tmin), canvas.height); - ctx.fill(); - ctx.stroke(); - - ctx.restore(); } -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; - - 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 = '' + event.id + '
'; - if (event.attrs) - { - event.attrs.forEach(function(attr) { - t += attr + '
'; - }); - } - return t; - }} (event) - }); - } - - ctx.restore(); -} - -function display_hierarchy(main_data, data, canvas, range, zoom) +function draw_history_graph() { + let canvas = document.getElementById("canvas_history"); canvas._tooltips = []; - var ctx = canvas.getContext('2d'); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.save(); + let context = canvas.getContext("2d"); + context.clearRect(0, 0, canvas.width, canvas.height); - ctx.font = '12px sans-serif'; + let legend = document.getElementById("history_graph").querySelector("aside"); + legend.innerHTML = ""; - 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 (!g_active_elements.length) + return; - if (range.tmin) + var series_data = {}; + var use_log_scale = null; + + var frames_nb = Infinity; + var x_scale = 0; + var y_scale = 0; + + var item_nb = 0; + + var tooltip_helper = {}; + + for (let typeI in g_active_elements) { - 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; - 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 label = interval.id; - if (interval.attrs) + for (let rep in g_reports) { - if (/^\d+$/.exec(interval.attrs[0])) - label += ' ' + interval.attrs[0]; + if (g_reports[rep].data().threads[g_main_thread].frames.length < frames_nb) + frames_nb = g_reports[rep].data().threads[g_main_thread].frames.length; + item_nb++; + let data = get_history_data(rep, g_main_thread, g_active_elements[typeI]); + series_data[rep + "/" + g_active_elements[typeI]] = smooth_1D_array(data.time_by_frame, 3); + if (data.max > y_scale) + y_scale = data.max; + if (use_log_scale === null && data.log_scale) + use_log_scale = true; + } + } + if (use_log_scale) + { + let legend_item = document.createElement("p"); + legend_item.style.borderColor = "transparent"; + legend_item.textContent = " -- log y scale -- "; + legend.appendChild(legend_item); + } + canvas.width = Math.max(frames_nb,600); + x_scale = frames_nb / canvas.width; + let id = 0; + for (let type in series_data) + { + let colour = graph_colour(id); + + let legend_item = document.createElement("p"); + legend_item.style.borderColor = colour; + legend_item.textContent = type; + legend.appendChild(legend_item); + + let time_by_frame = series_data[type]; + let last_val = 0; + for (var i = 0; i < frames_nb; i++) + { + let smoothed_time = time_by_frame[i];//smooth_1D(time_by_frame.slice(0), i, 3); + + let y = smoothed_time/y_scale; + if (use_log_scale) + y = Math.log10(1 + smoothed_time/y_scale * 9); + + if (item_nb === 1) + { + context.beginPath(); + context.fillStyle = colour; + context.fillRect(i/x_scale,canvas.height,1/x_scale,-y*canvas.height); + } else - label += ' [...]'; + { + if ( i == frames_nb-1) + continue; + context.globalCompositeOperation = "lighten"; + context.beginPath(); + context.strokeStyle = colour + context.lineWidth = 0.5; + context.moveTo(i/x_scale,canvas.height * (1 - last_val)); + context.lineTo((i+1)/x_scale,canvas.height * (1 - y)); + context.stroke(); + } + last_val = y; + if (!tooltip_helper[Math.floor(i/x_scale)]) + tooltip_helper[Math.floor(i/x_scale)] = []; + tooltip_helper[Math.floor(i/x_scale)].push([y, type]); } - var x0 = Math.floor(time_to_x(interval.t0)); - var x1 = Math.floor(time_to_x(interval.t1)); - var y0 = padding_top + interval.depth * BAR_SPACING; - var y1 = y0 + BAR_SPACING; - - 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)); - + id++; + } + + for (let i in tooltip_helper) + { + let tooltips = tooltip_helper[i]; + let text = "Frame " + i*x_scale + "
"; + for (let j in tooltips) + if (tooltips[j][0] != undefined && text.search(tooltips[j][1])===-1) + text += "Series " + tooltips[j][1] + ": " + time_label((tooltips[j][0])*y_scale,1) + "
"; canvas._tooltips.push({ - 'x0': x0, 'x1': x1, - 'y0': y0, 'y1': y1, - 'text': function(interval) { return function() { - var t = '' + interval.id + '
'; - t += 'Length: ' + time_label(interval.duration) + '
'; - if (interval.attrs) - { - interval.attrs.forEach(function(attr) { - t += attr + '
'; - }); - } - return t; - }} (interval) - }); - + 'x0': +i, 'x1': +i+1, + 'y0': 0, 'y1': canvas.height, + 'text': function(text) { return function() { return text; } }(text) + }); } + set_tooltip_handlers(canvas); - for (var i = 0; i < main_data.frames.length; ++i) + [0.1,0.25,0.5,0.75].forEach(function(y_val) { - var frame = main_data.frames[i]; + let y = y_val; + if (use_log_scale) + y = Math.log10(1 + y_val * 9); - 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(); + context.beginPath(); + context.lineWidth="1"; + context.strokeStyle = "rgba(0,0,0,0.2)"; + context.moveTo(0,canvas.height * (1- y)); + context.lineTo(canvas.width,canvas.height * (1 - y)); + context.stroke(); + context.fillStyle = "gray"; + context.font = "10px Arial"; + context.textAlign="left"; + context.fillText(time_label(y*y_scale,0), 2, canvas.height * (1 - y) - 2 ); + }); } -function set_frames_zoom_handlers(canvas0) +function compare_reports() { - function do_zoom(event) + let section = document.getElementById("comparison"); + section.innerHTML = "

Report Comparison

"; + + if (g_reports.length < 2) { - var zdata = canvas0._zoomData; - - var relativeX = event.pageX - this.offsetLeft; - var relativeY = event.pageY - this.offsetTop; - -// var width = 0.001 + 0.5 * relativeY / canvas0.height; - var width = 0.001 + 5 * relativeY / canvas0.height; - - 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(range); + section.innerHTML += "

Too few reports loaded

"; + return; } - var mouse_is_down = false; - $(canvas0).unbind(); - $(canvas0).mousedown(function(event) { - mouse_is_down = true; - do_zoom.call(this, event); - }); - $(canvas0).mouseup(function(event) { - mouse_is_down = false; - }); - $(canvas0).mousemove(function(event) { - if (mouse_is_down) - do_zoom.call(this, event); - }); + if (g_active_elements.length != 1) + { + section.innerHTML += "

Too many of too few elements selected

"; + return; + } + + let frames_nb = g_reports[0].data().threads[g_main_thread].frames.length; + for (let rep in g_reports) + if (g_reports[rep].data().threads[g_main_thread].frames.length < frames_nb) + frames_nb = g_reports[rep].data().threads[g_main_thread].frames.length; + + if (frames_nb != g_reports[0].data().threads[g_main_thread].frames.length) + section.innerHTML += "

Only the first " + frames_nb + " frames will be considered.

"; + + let reports_data = []; + + for (let rep in g_reports) + { + let raw_data = get_history_data(rep, g_main_thread, g_active_elements[0]).time_by_frame; + reports_data.push({"time_data" : raw_data.slice(0,frames_nb), "sorted_data" : raw_data.slice(0,frames_nb).sort((a,b) => a-b)}); + } + + let table_output = "" + for (let rep in reports_data) + { + let report = reports_data[rep]; + table_output += ""; + // median + table_output += "" + // max + table_output += "" + let frames_better = 0; + let frames_diff = 0; + for (let f in report.time_data) + { + if (report.time_data[f] <= reports_data[0].time_data[f]) + frames_better++; + frames_diff += report.time_data[f] - reports_data[0].time_data[f]; + } + table_output += ""; + table_output += ""; + } + section.innerHTML += table_output + "
Profiler VariableMedianMaximum% better framestime difference per frame
Report " + rep + (rep == 0 ? " (reference)":"") + "" + time_label(report.sorted_data[Math.floor(report.sorted_data.length/2)]) + "" + time_label(report.sorted_data[report.sorted_data.length-1]) + "" + (frames_better/frames_nb*100).toFixed(0) + "%" + time_label((frames_diff/frames_nb),2) + "
"; +} + +function recompute_choices(report, thread) +{ + var choices = document.getElementById("choices").querySelector("nav"); + choices.innerHTML = "

Choices

"; + var types = {}; + var data = report.data().threads[thread]; + + for (let i = 0; i < data.intervals.length; i++) + types[data.intervals[i].id] = 0; + + var sorted_keys = Object.keys(types).sort(); + + for (let key in sorted_keys) + { + let type = sorted_keys[key]; + let p = document.createElement("p"); + p.textContent = type; + if (g_active_elements.indexOf(p.textContent) !== -1) + p.className = "active"; + choices.appendChild(p); + p.onclick = function() + { + if (g_active_elements.indexOf(p.textContent) !== -1) + { + p.className = ""; + g_active_elements = g_active_elements.filter( x => x != p.textContent); + update_analysis(); + return; + } + g_active_elements.push(p.textContent); + p.className = "active"; + update_analysis(); + } + } + update_analysis(); +} + +function update_analysis() +{ + compare_reports(); + draw_history_graph(); + draw_frequency_graph(); } -function set_zoom_handlers(main_data, data, canvas0, canvas1) +function load_report_from_file(evt) { - 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; - var width = 8 + 64 * relativeY / canvas0.height; - 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); - } - - var mouse_is_down = false; - $(canvas0).mousedown(function(event) { - mouse_is_down = true; - do_zoom.call(this, event); - }); - $(canvas0).mouseup(function(event) { - mouse_is_down = false; - }); - $(canvas0).mousemove(function(event) { - if (mouse_is_down) - do_zoom.call(this, event); - }); + var file = evt.target.files[0]; + if (!file) + return; + load_report(false, file); + evt.target.value = null; } -function set_tooltip_handlers(canvas) +function load_report(trylive, file) { - function do_tooltip(event) - { - var tooltips = canvas._tooltips; - if (!tooltips) - return; - - var relativeX = event.pageX - this.offsetLeft; - var relativeY = event.pageY - this.offsetTop; + if (g_loading_timeout != undefined) + return; - 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'); - } - } + let reportID = g_reports.length; + let nav = document.querySelector("header nav"); + let newRep = document.createElement("p"); + newRep.textContent = file.name; + newRep.className = "loading"; + newRep.id = "Report" + reportID; + newRep.dataset.id = reportID; + nav.appendChild(newRep); - $(canvas).mousemove(function(event) { - do_tooltip.call(this, event); - }); + g_reports.push(Profiler2Report(on_report_loaded, trylive, file)); + g_loading_timeout = setTimeout(function() { on_report_loaded(false); }, 5000); } -function search_regions(query) +function on_report_loaded(success) { - var re = new RegExp(query); - var data = g_data.threads[0].processed_events; - - var found = []; - for (var i = 0; i < data.intervals.length; ++i) + let element = document.getElementById("Report" + (g_reports.length-1)); + let report = g_reports[g_reports.length-1]; + if (!success) { - var interval = data.intervals[i]; - if (interval.id.match(re)) - { - found.push(interval); - if (found.length > 100) - break; - } - } - - var out = $('#regionsearchresult > tbody'); - out.empty(); - for (var i = 0; i < found.length; ++i) - { - out.append($('?' + found[i].id + '' + (found[i].duration*1000) + '')); + element.className = "fail"; + setTimeout(function() { element.parentNode.removeChild(element); clearTimeout(g_loading_timeout); g_loading_timeout = null; }, 1000 ); + g_reports = g_reports.slice(0,-1); + if (g_reports.length === 0) + g_current_report = null; + return; } + clearTimeout(g_loading_timeout); + g_loading_timeout = null; + select_report(+element.dataset.id); + element.onclick = function() { select_report(+element.dataset.id);}; +} + +function select_report(id) +{ + if (g_current_report != undefined) + document.getElementById("Report" + g_current_report).className = ""; + document.getElementById("Report" + id).className = "active"; + g_current_report = id; + // Load up our canvas + g_report_draw.rebuild_canvases(g_reports[id].raw_data()); + g_report_draw.update_display(g_reports[id].data(),{"seconds":5}); + + recompute_choices(g_reports[id], g_main_thread); +} + +window.onload = function() +{ + // Try loading the report live + load_report(true, {"name":"live"}); + + // add new reports + document.getElementById('report_load_input').addEventListener('change', load_report_from_file, false); } \ No newline at end of file diff --git a/source/tools/profiler2/utilities.js b/source/tools/profiler2/utilities.js new file mode 100644 index 0000000000..a2e9f7f67a --- /dev/null +++ b/source/tools/profiler2/utilities.js @@ -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'); + }); +} \ No newline at end of file