Index: ps/trunk/source/ps/Profiler2.cpp =================================================================== --- ps/trunk/source/ps/Profiler2.cpp (revision 18422) +++ ps/trunk/source/ps/Profiler2.cpp (revision 18423) @@ -1,598 +1,999 @@ -/* 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 * "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. */ #include "precompiled.h" #include "Profiler2.h" #include "lib/allocators/shared_ptr.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Profiler2GPU.h" #include "third_party/mongoose/mongoose.h" #include +#include CProfiler2 g_Profiler2; // A human-recognisable pattern (for debugging) followed by random bytes (for uniqueness) const u8 CProfiler2::RESYNC_MAGIC[8] = {0x11, 0x22, 0x33, 0x44, 0xf4, 0x93, 0xbe, 0x15}; CProfiler2::CProfiler2() : m_Initialised(false), m_FrameNumber(0), m_MgContext(NULL), m_GPU(NULL) { } CProfiler2::~CProfiler2() { if (m_Initialised) Shutdown(); } /** * Mongoose callback. Run in an arbitrary thread (possibly concurrently with other requests). */ static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) { CProfiler2* profiler = (CProfiler2*)request_info->user_data; ENSURE(profiler); void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling const char* header200 = "HTTP/1.1 200 OK\r\n" "Access-Control-Allow-Origin: *\r\n" // TODO: not great for security "Content-Type: text/plain; charset=utf-8\r\n\r\n"; const char* header404 = "HTTP/1.1 404 Not Found\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Unrecognised URI"; const char* header400 = "HTTP/1.1 400 Bad Request\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Invalid request"; switch (event) { case MG_NEW_REQUEST: { std::stringstream stream; std::string uri = request_info->uri; - if (uri == "/overview") + + if (uri == "/download") + { + profiler->SaveToFile(); + } + else if (uri == "/overview") { profiler->ConstructJSONOverview(stream); } else if (uri == "/query") { if (!request_info->query_string) { mg_printf(conn, "%s (no query string)", header400); return handled; } // Identify the requested thread char buf[256]; int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), "thread", buf, ARRAY_SIZE(buf)); if (len < 0) { mg_printf(conn, "%s (no 'thread')", header400); return handled; } std::string thread(buf); const char* err = profiler->ConstructJSONResponse(stream, thread); if (err) { mg_printf(conn, "%s (%s)", header400, err); return handled; } } else { mg_printf(conn, "%s", header404); return handled; } mg_printf(conn, "%s", header200); std::string str = stream.str(); mg_write(conn, str.c_str(), str.length()); return handled; } case MG_HTTP_ERROR: return NULL; case MG_EVENT_LOG: // Called by Mongoose's cry() LOGERROR("Mongoose error: %s", request_info->log_message); return NULL; case MG_INIT_SSL: return NULL; default: debug_warn(L"Invalid Mongoose event type"); return NULL; } }; void CProfiler2::Initialise() { ENSURE(!m_Initialised); int err = pthread_key_create(&m_TLS, &CProfiler2::TLSDtor); ENSURE(err == 0); m_Initialised = true; RegisterCurrentThread("main"); } void CProfiler2::InitialiseGPU() { ENSURE(!m_GPU); m_GPU = new CProfiler2GPU(*this); } void CProfiler2::EnableHTTP() { ENSURE(m_Initialised); LOGMESSAGERENDER("Starting profiler2 HTTP server"); // Ignore multiple enablings if (m_MgContext) return; const char *options[] = { "listening_ports", "127.0.0.1:8000", // bind to localhost for security "num_threads", "6", // enough for the browser's parallel connection limit NULL }; m_MgContext = mg_start(MgCallback, this, options); ENSURE(m_MgContext); } void CProfiler2::EnableGPU() { ENSURE(m_Initialised); if (!m_GPU) { LOGMESSAGERENDER("Starting profiler2 GPU mode"); InitialiseGPU(); } } void CProfiler2::ShutdownGPU() { LOGMESSAGERENDER("Shutting down profiler2 GPU mode"); SAFE_DELETE(m_GPU); } void CProfiler2::ShutDownHTTP() { LOGMESSAGERENDER("Shutting down profiler2 HTTP server"); if (m_MgContext) { mg_stop(m_MgContext); m_MgContext = NULL; } } void CProfiler2::Toggle() { // TODO: Maybe we can open the browser to the profiler page automatically if (m_GPU && m_MgContext) { ShutdownGPU(); ShutDownHTTP(); } else if (!m_GPU && !m_MgContext) { EnableGPU(); EnableHTTP(); } } void CProfiler2::Shutdown() { ENSURE(m_Initialised); ENSURE(!m_GPU); // must shutdown GPU before profiler if (m_MgContext) { mg_stop(m_MgContext); m_MgContext = NULL; } // the destructor is not called for the main thread // we have to call it manually to avoid memory leaks ENSURE(ThreadUtil::IsMainThread()); void * dataptr = pthread_getspecific(m_TLS); TLSDtor(dataptr); int err = pthread_key_delete(m_TLS); ENSURE(err == 0); m_Initialised = false; } void CProfiler2::RecordGPUFrameStart() { if (m_GPU) m_GPU->FrameStart(); } void CProfiler2::RecordGPUFrameEnd() { if (m_GPU) m_GPU->FrameEnd(); } void CProfiler2::RecordGPURegionEnter(const char* id) { if (m_GPU) m_GPU->RegionEnter(id); } void CProfiler2::RecordGPURegionLeave(const char* id) { if (m_GPU) m_GPU->RegionLeave(id); } /** * Called by pthreads when a registered thread is destroyed. */ void CProfiler2::TLSDtor(void* data) { ThreadStorage* storage = (ThreadStorage*)data; storage->GetProfiler().RemoveThreadStorage(storage); delete (ThreadStorage*)data; } void CProfiler2::RegisterCurrentThread(const std::string& name) { ENSURE(m_Initialised); ENSURE(pthread_getspecific(m_TLS) == NULL); // mustn't register a thread more than once ThreadStorage* storage = new ThreadStorage(*this, name); int err = pthread_setspecific(m_TLS, storage); ENSURE(err == 0); RecordSyncMarker(); RecordEvent("thread start"); AddThreadStorage(storage); } void CProfiler2::AddThreadStorage(ThreadStorage* storage) { CScopeLock lock(m_Mutex); m_Threads.push_back(storage); } void CProfiler2::RemoveThreadStorage(ThreadStorage* storage) { CScopeLock lock(m_Mutex); m_Threads.erase(std::find(m_Threads.begin(), m_Threads.end(), 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); } 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). // // See comments on m_BufferPos0 etc. shared_ptr buffer(new u8[BUFFER_SIZE], ArrayDeleter()); u32 pos1 = m_BufferPos1; COMPILER_FENCE; // must read m_BufferPos1 before m_Buffer memcpy(buffer.get(), m_Buffer, BUFFER_SIZE); COMPILER_FENCE; // must read m_BufferPos0 after m_Buffer u32 pos0 = m_BufferPos0; // The range [pos1, pos0) modulo BUFFER_SIZE is invalid, so concatenate the rest of the buffer if (pos1 <= pos0) // invalid range is in the middle of the buffer return std::string(buffer.get()+pos0, buffer.get()+BUFFER_SIZE) + std::string(buffer.get(), buffer.get()+pos1); else // invalid wrap is wrapped around the end/start buffer return std::string(buffer.get()+pos0, buffer.get()+pos1); } void CProfiler2::ThreadStorage::RecordAttribute(const char* fmt, va_list argp) { char buffer[MAX_ATTRIBUTE_LENGTH + 4] = {0}; // first 4 bytes are used for storing length int len = vsnprintf(buffer + 4, MAX_ATTRIBUTE_LENGTH - 1, fmt, argp); // subtract 1 from length to make MSVC vsnprintf safe // (Don't use vsprintf_s because it treats overflow as fatal) // Terminate the string if the printing was truncated if (len < 0 || len >= (int)MAX_ATTRIBUTE_LENGTH - 1) { strncpy(buffer + 4 + MAX_ATTRIBUTE_LENGTH - 4, "...", 4); len = MAX_ATTRIBUTE_LENGTH - 1; // excluding null terminator } // Store the length in the buffer memcpy(buffer, &len, sizeof(len)); 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) { TIMER(L"profile2 overview"); CScopeLock lock(m_Mutex); stream << "{\"threads\":["; for (size_t i = 0; i < m_Threads.size(); ++i) { if (i != 0) stream << ","; stream << "{\"name\":\"" << CStr(m_Threads[i]->GetName()).EscapeToPrintableASCII() << "\"}"; } stream << "]}"; } /** * Given a buffer and a visitor class (with functions OnEvent, OnEnter, OnLeave, OnAttribute), * calls the visitor for every item in the buffer. */ template void RunBufferVisitor(const std::string& buffer, V& visitor) { TIMER(L"profile2 visitor"); // The buffer doesn't necessarily start at the beginning of an item // (we just grabbed it from some arbitrary point in the middle), // so scan forwards until we find a sync marker. // (This is probably pretty inefficient.) u32 realStart = (u32)-1; // the start point decided by the scan algorithm for (u32 start = 0; start + 1 + sizeof(CProfiler2::RESYNC_MAGIC) <= buffer.length(); ++start) { if (buffer[start] == CProfiler2::ITEM_SYNC && memcmp(buffer.c_str() + start + 1, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0) { realStart = start; break; } } ENSURE(realStart != (u32)-1); // we should have found a sync point somewhere in the buffer u32 pos = realStart; // the position as we step through the buffer double lastTime = -1; // set to non-negative by EVENT_SYNC; we ignore all items before that // since we can't compute their absolute times while (pos < buffer.length()) { u8 type = buffer[pos]; ++pos; switch (type) { case CProfiler2::ITEM_NOP: { // ignore break; } case CProfiler2::ITEM_SYNC: { u8 magic[sizeof(CProfiler2::RESYNC_MAGIC)]; double t; memcpy(magic, buffer.c_str()+pos, ARRAY_SIZE(magic)); ENSURE(memcmp(magic, &CProfiler2::RESYNC_MAGIC, sizeof(CProfiler2::RESYNC_MAGIC)) == 0); pos += sizeof(CProfiler2::RESYNC_MAGIC); memcpy(&t, buffer.c_str()+pos, sizeof(t)); pos += sizeof(t); lastTime = t; visitor.OnSync(lastTime); break; } case CProfiler2::ITEM_EVENT: { CProfiler2::SItem_dt_id item; memcpy(&item, buffer.c_str()+pos, sizeof(item)); 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; } case CProfiler2::ITEM_ENTER: { CProfiler2::SItem_dt_id item; memcpy(&item, buffer.c_str()+pos, sizeof(item)); 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; } case CProfiler2::ITEM_ATTRIBUTE: { u32 len; memcpy(&len, buffer.c_str()+pos, sizeof(len)); ENSURE(len <= CProfiler2::MAX_ATTRIBUTE_LENGTH); pos += sizeof(len); std::string attribute(buffer.c_str()+pos, buffer.c_str()+pos+len); pos += len; if (lastTime >= 0) { visitor.OnAttribute(attribute); } break; } default: debug_warn(L"Invalid profiler item when parsing buffer"); return; } } }; /** * Visitor class that dumps events as JSON. * TODO: this is pretty inefficient (in implementation and in output format). */ struct BufferVisitor_Dump { NONCOPYABLE(BufferVisitor_Dump); public: BufferVisitor_Dump(std::ostream& stream) : m_Stream(stream) { } void OnSync(double UNUSED(time)) { // Split the array of items into an array of array (arbitrarily splitting // around the sync points) to avoid array-too-large errors in JSON decoders m_Stream << "null], [\n"; } void OnEvent(double time, const char* id) { m_Stream << "[1," << std::fixed << std::setprecision(9) << time; m_Stream << ",\"" << CStr(id).EscapeToPrintableASCII() << "\"],\n"; } void OnEnter(double time, const char* id) { m_Stream << "[2," << std::fixed << std::setprecision(9) << time; 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) { m_Stream << "[4,\"" << CStr(attr).EscapeToPrintableASCII() << "\"],\n"; } std::ostream& m_Stream; }; const char* CProfiler2::ConstructJSONResponse(std::ostream& stream, const std::string& thread) { TIMER(L"profile2 query"); std::string buffer; { TIMER(L"profile2 get buffer"); CScopeLock lock(m_Mutex); // lock against changes to m_Threads or deletions of ThreadStorage ThreadStorage* storage = NULL; for (size_t i = 0; i < m_Threads.size(); ++i) { if (m_Threads[i]->GetName() == thread) { storage = m_Threads[i]; break; } } if (!storage) return "cannot find named thread"; stream << "{\"events\":[\n"; stream << "[\n"; buffer = storage->GetBuffer(); } BufferVisitor_Dump visitor(stream); RunBufferVisitor(buffer, visitor); stream << "null]\n]}"; return NULL; } void CProfiler2::SaveToFile() { OsPath path = psLogDir()/"profile2.jsonp"; std::ofstream stream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); ENSURE(stream.good()); std::vector threads; { CScopeLock lock(m_Mutex); threads = m_Threads; } stream << "profileDataCB({\"threads\": [\n"; for (size_t i = 0; i < threads.size(); ++i) { if (i != 0) stream << ",\n"; stream << "{\"name\":\"" << CStr(threads[i]->GetName()).EscapeToPrintableASCII() << "\",\n"; stream << "\"data\": "; ConstructJSONResponse(stream, threads[i]->GetName()); stream << "\n}"; } 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); +} Index: ps/trunk/source/ps/Profiler2.h =================================================================== --- ps/trunk/source/ps/Profiler2.h (revision 18422) +++ ps/trunk/source/ps/Profiler2.h (revision 18423) @@ -1,473 +1,556 @@ -/* 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 * "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. */ /** * @file * New profiler (complementing the older CProfileManager) * * The profiler is designed for analysing framerate fluctuations or glitches, * and temporal relationships between threads. * This contrasts with CProfilerManager and most external profiling tools, * which are designed more for measuring average throughput over a number of * frames. * * To view the profiler output, press F11 to enable the HTTP output mode * and then open source/tools/profiler2/profiler2.html in a web browser. * * There is a single global CProfiler2 instance (g_Profiler2), * providing the API used by the rest of the game. * The game can record the entry/exit timings of a region of code * using the PROFILE2 macro, and can record other events using * PROFILE2_EVENT. * Regions and events can be annotated with arbitrary string attributes, * specified with printf-style format strings, using PROFILE2_ATTR * (e.g. PROFILE2_ATTR("frame: %d", m_FrameNum) ). * * This is designed for relatively coarse-grained profiling, or for rare events. * Don't use it for regions that are typically less than ~0.1msecs, or that are * called hundreds of times per frame. (The old CProfilerManager is better * for that.) * * New threads must call g_Profiler2.RegisterCurrentThread before any other * profiler functions. * * The main thread should call g_Profiler2.RecordFrameStart at the start of * each frame. * Other threads should call g_Profiler2.RecordSyncMarker occasionally, * especially if it's been a long time since their last call to the profiler, * or if they've made thousands of calls since the last sync marker. * * The profiler is implemented with thread-local fixed-size ring buffers, * which store a sequence of variable-length items indicating the time * of the event and associated data (pointers to names, attribute strings, etc). * An HTTP server provides access to the data: when requested, it will make * a copy of a thread's buffer, then parse the items and return them in JSON * format. The profiler2.html requests and processes and visualises this data. * * The RecordSyncMarker calls are necessary to correct for time drift and to * let the buffer parser accurately detect the start of an item in the byte stream. * * This design aims to minimise the performance overhead of recording data, * and to simplify the visualisation of the data by doing it externally in an * environment with better UI tools (i.e. HTML) instead of within the game engine. * * The initial setup of g_Profiler2 must happen in the game's main thread. * RegisterCurrentThread and the Record functions may be called from any thread. * The HTTP server runs its own threads, which may call the ConstructJSON functions. */ #ifndef INCLUDED_PROFILER2 #define INCLUDED_PROFILER2 #include "lib/timer.h" #include "ps/ThreadUtil.h" struct mg_context; // Note: Lots of functions are defined inline, to hypothetically // minimise performance overhead. class CProfiler2GPU; class CProfiler2 { friend class CProfiler2GPU_base; - + friend class CProfile2SpikeRegion; + friend class CProfile2AggregatedRegion; public: // Items stored in the buffers: /// Item type identifiers enum EItem { ITEM_NOP = 0, ITEM_SYNC = 1, // magic value used for parse syncing ITEM_EVENT = 2, // single event ITEM_ENTER = 3, // entering a region ITEM_LEAVE = 4, // leaving a region (must be correctly nested) ITEM_ATTRIBUTE = 5, // arbitrary string associated with current region, or latest event (if the previous item was an event) }; static const size_t MAX_ATTRIBUTE_LENGTH = 256; // includes null terminator, which isn't stored /// An arbitrary number to help resyncing with the item stream when parsing. static const u8 RESYNC_MAGIC[8]; /** * An item with a relative time and an ID string pointer. */ struct SItem_dt_id { float dt; // time relative to last event const char* id; }; 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. */ class ThreadStorage { NONCOPYABLE(ThreadStorage); public: 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 // (to correct for drift caused by the precision of relative // times stored in other items) u8 buffer[sizeof(RESYNC_MAGIC) + sizeof(t)]; memcpy(buffer, &RESYNC_MAGIC, sizeof(RESYNC_MAGIC)); memcpy(buffer + sizeof(RESYNC_MAGIC), &t, sizeof(t)); Write(ITEM_SYNC, buffer, ARRAY_SIZE(buffer)); m_LastTime = t; } void Record(EItem type, double t, const char* id) { // Store a relative time instead of absolute, so we can use floats // (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) { RecordSyncMarker(t); 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) { va_list argp; va_start(argp, fmt); RecordAttribute(fmt, argp); va_end(argp); } + size_t HoldLevel(); + u8 HoldType(); + void PutOnHold(u8 type); + void HoldToBuffer(bool condensed); + void ThrowawayHoldBuffer(); + CProfiler2& GetProfiler() { return m_Profiler; } const std::string& GetName() { return m_Name; } /** * Returns a copy of a subset of the thread's buffer. * Not guaranteed to start on an item boundary. * May be called by any thread. */ std::string GetBuffer(); 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; double m_LastTime; // used for computing relative times 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. // BufferPos0 is updated before writing; BufferPos1 is updated after writing. // GetBuffer can read Pos1, copy the buffer, read Pos0, then assume any bytes // outside the range Pos1 <= x < Pos0 are safe to use. (Any in that range might // be half-written and corrupted.) (All ranges are modulo BUFFER_SIZE.) // Outside of Write(), these will always be equal. // // TODO: does this attempt at synchronisation (plus use of COMPILER_FENCE etc) // actually work in practice? u32 m_BufferPos0; u32 m_BufferPos1; }; public: CProfiler2(); ~CProfiler2(); /** * Call in main thread to set up the profiler, * before calling any other profiler functions. */ void Initialise(); /** * Call in main thread to enable the HTTP server. * (Disabled by default for security and performance * and to avoid annoying a firewall.) */ void EnableHTTP(); /** * Call in main thread to enable the GPU profiling support, * after OpenGL has been initialised. */ void EnableGPU(); /** * Call in main thread to shut down the GPU profiling support, * before shutting down OpenGL. */ void ShutdownGPU(); /** * Call in main thread to shut down the profiler's HTTP server */ void ShutDownHTTP(); /** * Call in main thread to enable/disable the profiler */ void Toggle(); /** * Call in main thread to shut everything down. * All other profiled threads should have been terminated already. */ void Shutdown(); /** * Call in any thread to enable the profiler in that thread. * @p name should be unique, and is used by the visualiser to identify * this thread. */ void RegisterCurrentThread(const std::string& name); /** * Non-main threads should call this occasionally, * especially if it's been a long time since their last call to the profiler, * or if they've made thousands of calls since the last sync marker. */ void RecordSyncMarker() { GetThreadStorage().RecordSyncMarker(GetTime()); } /** * Call in main thread at the start of a frame. */ void RecordFrameStart() { ENSURE(ThreadUtil::IsMainThread()); GetThreadStorage().RecordFrameStart(GetTime()); } void RecordEvent(const char* id) { GetThreadStorage().Record(ITEM_EVENT, GetTime(), id); } void RecordRegionEnter(const char* id) { GetThreadStorage().Record(ITEM_ENTER, GetTime(), id); } - void RecordRegionLeave(const char* id) + void RecordRegionEnter(const char* id, double time) + { + GetThreadStorage().Record(ITEM_ENTER, time, id); + } + + void RecordRegionLeave() { - GetThreadStorage().Record(ITEM_LEAVE, GetTime(), id); + GetThreadStorage().RecordLeave(GetTime()); } void RecordAttribute(const char* fmt, ...) PRINTF_ARGS(2) { va_list argp; va_start(argp, fmt); GetThreadStorage().RecordAttribute(fmt, argp); va_end(argp); } void RecordGPUFrameStart(); void RecordGPUFrameEnd(); 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. */ void ConstructJSONOverview(std::ostream& stream); /** * Call in any thread to produce a JSON representation of the buffer * for a given thread. * Returns NULL on success, or an error string. */ const char* ConstructJSONResponse(std::ostream& stream, const std::string& thread); /** * Call in any thread to save a JSONP representation of the buffers * for all threads, to a file named profile2.jsonp in the logs directory. */ void SaveToFile(); double GetTime() { return timer_Time(); } int GetFrameNumber() { return m_FrameNumber; } void IncrementFrameNumber() { ++m_FrameNumber; } void AddThreadStorage(ThreadStorage* storage); void RemoveThreadStorage(ThreadStorage* storage); private: void InitialiseGPU(); static void TLSDtor(void* data); ThreadStorage& GetThreadStorage() { ThreadStorage* storage = (ThreadStorage*)pthread_getspecific(m_TLS); ASSERT(storage); return *storage; } bool m_Initialised; int m_FrameNumber; mg_context* m_MgContext; pthread_key_t m_TLS; CProfiler2GPU* m_GPU; CMutex m_Mutex; std::vector m_Threads; // thread-safe; protected by m_Mutex }; extern CProfiler2 g_Profiler2; /** * Scope-based enter/leave helper. */ class CProfile2Region { public: CProfile2Region(const char* name) : m_Name(name) { g_Profiler2.RecordRegionEnter(m_Name); } ~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; }; /** * Scope-based GPU enter/leave helper. */ class CProfile2GPURegion { public: CProfile2GPURegion(const char* name) : m_Name(name) { g_Profiler2.RecordGPURegionEnter(m_Name); } ~CProfile2GPURegion() { g_Profiler2.RecordGPURegionLeave(m_Name); } private: const char* m_Name; }; /** * Starts timing from now until the end of the current scope. * @p region is the name to associate with this region (should be * a constant string literal; the pointer must remain valid forever). * Regions may be nested, but preferably shouldn't be nested deeply since * it hurts the visualisation. */ #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) /** * Record the named event at the current time. */ #define PROFILE2_EVENT(name) g_Profiler2.RecordEvent(name) /** * Associates a string (with printf-style formatting) with the current * region or event. * (If the last profiler call was PROFILE2_EVENT, it associates with that * event; otherwise it associates with the currently-active region.) */ #define PROFILE2_ATTR g_Profiler2.RecordAttribute #endif // INCLUDED_PROFILER2 Index: ps/trunk/source/ps/UserReport.cpp =================================================================== --- ps/trunk/source/ps/UserReport.cpp (revision 18422) +++ ps/trunk/source/ps/UserReport.cpp (revision 18423) @@ -1,629 +1,629 @@ -/* 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 * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "UserReport.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/external_libraries/curl.h" #include "lib/external_libraries/libsdl.h" #include "lib/external_libraries/zlib.h" #include "lib/file/archive/stream.h" #include "lib/sysdep/sysdep.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Profiler2.h" #include "ps/ThreadUtil.h" #define DEBUG_UPLOADS 0 /* * The basic idea is that the game submits reports to us, which we send over * HTTP to a server for storage and analysis. * * We can't use libcurl's asynchronous 'multi' API, because DNS resolution can * be synchronous and slow (which would make the game pause). * So we use the 'easy' API in a background thread. * The main thread submits reports, toggles whether uploading is enabled, * and polls for the current status (typically to display in the GUI); * the worker thread does all of the uploading. * * It'd be nice to extend this in the future to handle things like crash reports. * The game should store the crashlogs (suitably anonymised) in a directory, and * we should detect those files and upload them when we're restarted and online. */ /** * Version number stored in config file when the user agrees to the reporting. * Reporting will be disabled if the config value is missing or is less than * this value. If we start reporting a lot more data, we should increase this * value and get the user to re-confirm. */ static const int REPORTER_VERSION = 1; /** * Time interval (seconds) at which the worker thread will check its reconnection * timers. (This should be relatively high so the thread doesn't waste much time * continually waking up.) */ static const double TIMER_CHECK_INTERVAL = 10.0; /** * Seconds we should wait before reconnecting to the server after a failure. */ static const double RECONNECT_INVERVAL = 60.0; CUserReporter g_UserReporter; struct CUserReport { time_t m_Time; std::string m_Type; int m_Version; std::string m_Data; }; class CUserReporterWorker { public: CUserReporterWorker(const std::string& userID, const std::string& url) : m_URL(url), m_UserID(userID), m_Enabled(false), m_Shutdown(false), m_Status("disabled"), m_PauseUntilTime(timer_Time()), m_LastUpdateTime(timer_Time()) { // Set up libcurl: m_Curl = curl_easy_init(); ENSURE(m_Curl); #if DEBUG_UPLOADS curl_easy_setopt(m_Curl, CURLOPT_VERBOSE, 1L); #endif // Capture error messages curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer); // Disable signal handlers (required for multithreaded applications) curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L); // To minimise security risks, don't support redirects curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L); // Set IO callbacks curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback); curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this); curl_easy_setopt(m_Curl, CURLOPT_READFUNCTION, SendCallback); curl_easy_setopt(m_Curl, CURLOPT_READDATA, this); // Set URL to POST to curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(m_Curl, CURLOPT_POST, 1L); // Set up HTTP headers m_Headers = NULL; // Set the UA string std::string ua = "User-Agent: 0ad "; ua += curl_version(); ua += " (http://play0ad.com/)"; m_Headers = curl_slist_append(m_Headers, ua.c_str()); // Override the default application/x-www-form-urlencoded type since we're not using that type m_Headers = curl_slist_append(m_Headers, "Content-Type: application/octet-stream"); // Disable the Accept header because it's a waste of a dozen bytes m_Headers = curl_slist_append(m_Headers, "Accept: "); curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers); // Set up the worker thread: // Use SDL semaphores since OS X doesn't implement sem_init m_WorkerSem = SDL_CreateSemaphore(0); ENSURE(m_WorkerSem); int ret = pthread_create(&m_WorkerThread, NULL, &RunThread, this); ENSURE(ret == 0); } ~CUserReporterWorker() { // Clean up resources SDL_DestroySemaphore(m_WorkerSem); curl_slist_free_all(m_Headers); curl_easy_cleanup(m_Curl); } /** * Called by main thread, when the online reporting is enabled/disabled. */ void SetEnabled(bool enabled) { CScopeLock lock(m_WorkerMutex); if (enabled != m_Enabled) { m_Enabled = enabled; // Wake up the worker thread SDL_SemPost(m_WorkerSem); } } /** * Called by main thread to request shutdown. * Returns true if we've shut down successfully. * Returns false if shutdown is taking too long (we might be blocked on a * sync network operation) - you mustn't destroy this object, just leak it * and terminate. */ bool Shutdown() { { CScopeLock lock(m_WorkerMutex); m_Shutdown = true; } // Wake up the worker thread SDL_SemPost(m_WorkerSem); // Wait for it to shut down cleanly // TODO: should have a timeout in case of network hangs pthread_join(m_WorkerThread, NULL); return true; } /** * Called by main thread to determine the current status of the uploader. */ std::string GetStatus() { CScopeLock lock(m_WorkerMutex); return m_Status; } /** * Called by main thread to add a new report to the queue. */ void Submit(const shared_ptr& report) { { CScopeLock lock(m_WorkerMutex); m_ReportQueue.push_back(report); } // Wake up the worker thread SDL_SemPost(m_WorkerSem); } /** * Called by the main thread every frame, so we can check * retransmission timers. */ void Update() { double now = timer_Time(); if (now > m_LastUpdateTime + TIMER_CHECK_INTERVAL) { // Wake up the worker thread SDL_SemPost(m_WorkerSem); m_LastUpdateTime = now; } } private: static void* RunThread(void* data) { debug_SetThreadName("CUserReportWorker"); g_Profiler2.RegisterCurrentThread("userreport"); static_cast(data)->Run(); return NULL; } void Run() { // Set libcurl's proxy configuration // (This has to be done in the thread because it's potentially very slow) SetStatus("proxy"); std::wstring proxy; { PROFILE2("get proxy config"); if (sys_get_proxy_config(wstring_from_utf8(m_URL), proxy) == INFO::OK) curl_easy_setopt(m_Curl, CURLOPT_PROXY, utf8_from_wstring(proxy).c_str()); } SetStatus("waiting"); /* * We use a semaphore to let the thread be woken up when it has * work to do. Various actions from the main thread can wake it: * * SetEnabled() * * Shutdown() * * Submit() * * Retransmission timeouts, once every several seconds * * If multiple actions have triggered wakeups, we might respond to * all of those actions after the first wakeup, which is okay (we'll do * nothing during the subsequent wakeups). We should never hang due to * processing fewer actions than wakeups. * * Retransmission timeouts are triggered via the main thread - we can't simply * use SDL_SemWaitTimeout because on Linux it's implemented as an inefficient * busy-wait loop, and we can't use a manual busy-wait with a long delay time * because we'd lose responsiveness. So the main thread pings the worker * occasionally so it can check its timer. */ g_Profiler2.RecordRegionEnter("semaphore wait"); // 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()) return; // If we're not enabled, ignore this wakeup if (!GetEnabled()) continue; // If we're still pausing due to a failed connection, // go back to sleep again if (timer_Time() < m_PauseUntilTime) continue; // We're enabled, so process as many reports as possible while (ProcessReport()) { // Handle shutdowns while we were sending the report if (GetShutdown()) return; } } - g_Profiler2.RecordRegionLeave("semaphore wait"); + g_Profiler2.RecordRegionLeave(); } bool GetEnabled() { CScopeLock lock(m_WorkerMutex); return m_Enabled; } bool GetShutdown() { CScopeLock lock(m_WorkerMutex); return m_Shutdown; } void SetStatus(const std::string& status) { CScopeLock lock(m_WorkerMutex); m_Status = status; #if DEBUG_UPLOADS debug_printf(">>> CUserReporterWorker status: %s\n", status.c_str()); #endif } bool ProcessReport() { PROFILE2("process report"); shared_ptr report; { CScopeLock lock(m_WorkerMutex); if (m_ReportQueue.empty()) return false; report = m_ReportQueue.front(); m_ReportQueue.pop_front(); } ConstructRequestData(*report); m_RequestDataOffset = 0; m_ResponseData.clear(); curl_easy_setopt(m_Curl, CURLOPT_POSTFIELDSIZE_LARGE, (curl_off_t)m_RequestData.size()); SetStatus("connecting"); #if DEBUG_UPLOADS TIMER(L"CUserReporterWorker request"); #endif CURLcode err = curl_easy_perform(m_Curl); #if DEBUG_UPLOADS printf(">>>\n%s\n<<<\n", m_ResponseData.c_str()); #endif if (err == CURLE_OK) { long code = -1; curl_easy_getinfo(m_Curl, CURLINFO_RESPONSE_CODE, &code); SetStatus("completed:" + CStr::FromInt(code)); // Check for success code if (code == 200) return true; // If the server returns the 410 Gone status, interpret that as meaning // it no longer supports uploads (at least from this version of the game), // so shut down and stop talking to it (to avoid wasting bandwidth) if (code == 410) { CScopeLock lock(m_WorkerMutex); m_Shutdown = true; return false; } } else { SetStatus("failed:" + CStr::FromInt(err) + ":" + m_ErrorBuffer); } // We got an unhandled return code or a connection failure; // push this report back onto the queue and try again after // a long interval { CScopeLock lock(m_WorkerMutex); m_ReportQueue.push_front(report); } m_PauseUntilTime = timer_Time() + RECONNECT_INVERVAL; return false; } void ConstructRequestData(const CUserReport& report) { // Construct the POST request data in the application/x-www-form-urlencoded format std::string r; r += "user_id="; AppendEscaped(r, m_UserID); r += "&time=" + CStr::FromInt64(report.m_Time); r += "&type="; AppendEscaped(r, report.m_Type); r += "&version=" + CStr::FromInt(report.m_Version); r += "&data="; AppendEscaped(r, report.m_Data); // Compress the content with zlib to save bandwidth. // (Note that we send a request with unlabelled compressed data instead // of using Content-Encoding, because Content-Encoding is a mess and causes // problems with servers and breaks Content-Length and this is much easier.) std::string compressed; compressed.resize(compressBound(r.size())); uLongf destLen = compressed.size(); int ok = compress((Bytef*)compressed.c_str(), &destLen, (const Bytef*)r.c_str(), r.size()); ENSURE(ok == Z_OK); compressed.resize(destLen); m_RequestData.swap(compressed); } void AppendEscaped(std::string& buffer, const std::string& str) { char* escaped = curl_easy_escape(m_Curl, str.c_str(), str.size()); buffer += escaped; curl_free(escaped); } static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp) { CUserReporterWorker* self = static_cast(userp); if (self->GetShutdown()) return 0; // signals an error self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb); return size*nmemb; } static size_t SendCallback(char* bufptr, size_t size, size_t nmemb, void* userp) { CUserReporterWorker* self = static_cast(userp); if (self->GetShutdown()) return CURL_READFUNC_ABORT; // signals an error // We can return as much data as available, up to the buffer size size_t amount = std::min(self->m_RequestData.size() - self->m_RequestDataOffset, size*nmemb); // ...But restrict to sending a small amount at once, so that we remain // responsive to shutdown requests even if the network is pretty slow amount = std::min((size_t)1024, amount); if(amount != 0) // (avoids invalid operator[] call where index=size) { memcpy(bufptr, &self->m_RequestData[self->m_RequestDataOffset], amount); self->m_RequestDataOffset += amount; } self->SetStatus("sending:" + CStr::FromDouble((double)self->m_RequestDataOffset / self->m_RequestData.size())); return amount; } private: // Thread-related members: pthread_t m_WorkerThread; CMutex m_WorkerMutex; SDL_sem* m_WorkerSem; // Shared by main thread and worker thread: // These variables are all protected by m_WorkerMutex std::deque > m_ReportQueue; bool m_Enabled; bool m_Shutdown; std::string m_Status; // Initialised in constructor by main thread; otherwise used only by worker thread: std::string m_URL; std::string m_UserID; CURL* m_Curl; curl_slist* m_Headers; double m_PauseUntilTime; // Only used by worker thread: std::string m_ResponseData; std::string m_RequestData; size_t m_RequestDataOffset; char m_ErrorBuffer[CURL_ERROR_SIZE]; // Only used by main thread: double m_LastUpdateTime; }; CUserReporter::CUserReporter() : m_Worker(NULL) { } CUserReporter::~CUserReporter() { ENSURE(!m_Worker); // Deinitialize should have been called before shutdown } std::string CUserReporter::LoadUserID() { std::string userID; // Read the user ID from user.cfg (if there is one) CFG_GET_VAL("userreport.id", userID); // If we don't have a validly-formatted user ID, generate a new one if (userID.length() != 16) { u8 bytes[8] = {0}; sys_generate_random_bytes(bytes, ARRAY_SIZE(bytes)); // ignore failures - there's not much we can do about it userID = ""; for (size_t i = 0; i < ARRAY_SIZE(bytes); ++i) { char hex[3]; sprintf_s(hex, ARRAY_SIZE(hex), "%02x", (unsigned int)bytes[i]); userID += hex; } g_ConfigDB.SetValueString(CFG_USER, "userreport.id", userID); g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.id", userID); } return userID; } bool CUserReporter::IsReportingEnabled() { int version = -1; CFG_GET_VAL("userreport.enabledversion", version); return (version >= REPORTER_VERSION); } void CUserReporter::SetReportingEnabled(bool enabled) { CStr val = CStr::FromInt(enabled ? REPORTER_VERSION : 0); g_ConfigDB.SetValueString(CFG_USER, "userreport.enabledversion", val); g_ConfigDB.WriteValueToFile(CFG_USER, "userreport.enabledversion", val); if (m_Worker) m_Worker->SetEnabled(enabled); } std::string CUserReporter::GetStatus() { if (!m_Worker) return "disabled"; return m_Worker->GetStatus(); } void CUserReporter::Initialize() { ENSURE(!m_Worker); // must only be called once std::string userID = LoadUserID(); std::string url; CFG_GET_VAL("userreport.url", url); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); m_Worker = new CUserReporterWorker(userID, url); m_Worker->SetEnabled(IsReportingEnabled()); } void CUserReporter::Deinitialize() { if (!m_Worker) return; if (m_Worker->Shutdown()) { // Worker was shut down cleanly SAFE_DELETE(m_Worker); curl_global_cleanup(); } else { // Worker failed to shut down in a reasonable time // Leak the resources (since that's better than hanging or crashing) m_Worker = NULL; } } void CUserReporter::Update() { if (m_Worker) m_Worker->Update(); } void CUserReporter::SubmitReport(const char* type, int version, const std::string& data) { // If not initialised, discard the report if (!m_Worker) return; shared_ptr report(new CUserReport); report->m_Time = time(NULL); report->m_Type = type; report->m_Version = version; report->m_Data = data; m_Worker->Submit(report); } Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 18422) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 18423) @@ -1,1135 +1,1135 @@ /* 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 * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptInterface.h" #include "ScriptRuntime.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" #include "scriptinterface/ScriptExtraHeaders.h" /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ struct ScriptInterface_impl { ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& runtime); ~ScriptInterface_impl(); void Register(const char* name, JSNative fptr, uint nargs); // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the runtime destructor. shared_ptr m_runtime; JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object JSCompartment* m_comp; boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object // TODO: we need DefPersistentRooted to work around a problem with JS::PersistentRooted // that is already solved in newer versions of SpiderMonkey (related to std::pair and // and the copy constructor of PersistentRooted taking a non-const reference). // Switch this to PersistentRooted when upgrading to a newer SpiderMonkey version than v31. typedef std::map > ScriptValCache; ScriptValCache m_ScriptValCache; }; namespace { JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, JS_PropertyStub, JS_DeletePropertyStub, JS_PropertyStub, JS_StrictPropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; void ErrorReporter(JSContext* cx, const char* message, JSErrorReport* report) { std::stringstream msg; bool isWarning = JSREPORT_IS_WARNING(report->flags); msg << (isWarning ? "JavaScript warning: " : "JavaScript error: "); if (report->filename) { msg << report->filename; msg << " line " << report->lineno << "\n"; } msg << message; // If there is an exception, then print its stack trace JS::RootedValue excn(cx); if (JS_GetPendingException(cx, &excn) && excn.isObject()) { JS::RootedObject excnObj(cx, &excn.toObject()); // TODO: this violates the docs ("The error reporter callback must not reenter the JSAPI.") // Hide the exception from EvaluateScript JSExceptionState* excnState = JS_SaveExceptionState(cx); JS_ClearPendingException(cx); JS::RootedValue rval(cx); const char dumpStack[] = "this.stack.trimRight().replace(/^/mg, ' ')"; // indent each line JS::CompileOptions opts(cx); if (JS::Evaluate(cx, excnObj, opts.setFileAndLine("(eval)", 1), dumpStack, ARRAY_SIZE(dumpStack)-1, &rval)) { std::string stackTrace; if (ScriptInterface::FromJSVal(cx, rval, stackTrace)) msg << "\n" << stackTrace; JS_RestoreExceptionState(cx, excnState); } else { // Error got replaced by new exception from EvaluateScript JS_DropExceptionState(cx, excnState); } } if (isWarning) LOGWARNING("%s", msg.str().c_str()); else LOGERROR("%s", msg.str().c_str()); // When running under Valgrind, print more information in the error message // VALGRIND_PRINTF_BACKTRACE("->"); } // Functions in the global namespace: bool print(JSContext* cx, uint argc, jsval* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::FromJSVal(cx, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, jsval* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } std::wstring str; if (!ScriptInterface::FromJSVal(cx, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, jsval* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } std::wstring str; if (!ScriptInterface::FromJSVal(cx, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, jsval* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } std::wstring str; if (!ScriptInterface::FromJSVal(cx, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool deepcopy(JSContext* cx, uint argc, jsval* vp) { JSAutoRequest rq(cx); JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } JS::RootedValue ret(cx); if (!JS_StructuredClone(cx, args[0], &ret, NULL, NULL)) return false; args.rval().set(ret); return true; } bool ProfileStart(JSContext* cx, uint argc, jsval* vp) { const char* name = "(ProfileStart)"; JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() >= 1) { std::string str; if (!ScriptInterface::FromJSVal(cx, args[0], str)) return false; typedef boost::flyweight< std::string, boost::flyweights::no_tracking, boost::flyweights::no_locking > StringFlyweight; name = StringFlyweight(str).get().c_str(); } if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); args.rval().setUndefined(); return true; } bool ProfileStop(JSContext* UNUSED(cx), uint UNUSED(argc), jsval* vp) { JS::CallReceiver rec = JS::CallReceiverFromVp(vp); if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); - g_Profiler2.RecordRegionLeave("(ProfileStop)"); + g_Profiler2.RecordRegionLeave(); rec.rval().setUndefined(); return true; } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } bool Math_random(JSContext* cx, uint UNUSED(argc), jsval* vp) { JS::CallReceiver rec = JS::CallReceiverFromVp(vp); double r; if(!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) return false; rec.rval().setNumber(r); return true; } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) { if (m->m_rng == NULL) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& runtime) : m_runtime(runtime), m_glob(runtime->m_rt), m_nativeScope(runtime->m_rt) { bool ok; m_cx = JS_NewContext(m_runtime->m_rt, STACK_CHUNK_SIZE); ENSURE(m_cx); JS_SetParallelIonCompilationEnabled(m_runtime->m_rt, true); // For GC debugging: // JS_SetGCZeal(m_cx, 2, JS_DEFAULT_ZEAL_FREQ); JS_SetContextPrivate(m_cx, NULL); JS_SetErrorReporter(m_cx, ErrorReporter); JS_SetGlobalJitCompilerOption(m_runtime->m_rt, JSJITCOMPILER_ION_ENABLE, 1); JS_SetGlobalJitCompilerOption(m_runtime->m_rt, JSJITCOMPILER_BASELINE_ENABLE, 1); JS::ContextOptionsRef(m_cx).setExtraWarnings(1) .setWerror(0) .setVarObjFix(1) .setStrictMode(1); JS::CompartmentOptions opt; opt.setVersion(JSVERSION_LATEST); JSAutoRequest rq(m_cx); JS::RootedObject globalRootedVal(m_cx, JS_NewGlobalObject(m_cx, &global_class, NULL, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt)); m_comp = JS_EnterCompartment(m_cx, globalRootedVal); ok = JS_InitStandardClasses(m_cx, globalRootedVal); ENSURE(ok); m_glob = globalRootedVal.get(); // Use the testing functions to globally enable gcPreserveCode. This brings quite a // big performance improvement. In future SpiderMonkey versions, we should probably // use the functions implemented here: https://bugzilla.mozilla.org/show_bug.cgi?id=1068697 JS::RootedObject testingFunctionsObj(m_cx, js::GetTestingFunctions(m_cx)); ENSURE(testingFunctionsObj); JS::RootedValue ret(m_cx); JS_CallFunctionName(m_cx, testingFunctionsObj, "gcPreserveCode", JS::HandleValueArray::empty(), &ret); JS_DefineProperty(m_cx, m_glob, "global", globalRootedVal, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, NULL, NULL, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, globalRootedVal, "deepcopy", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); Register("ProfileStart", ::ProfileStart, 1); Register("ProfileStop", ::ProfileStop, 0); runtime->RegisterContext(m_cx); } ScriptInterface_impl::~ScriptInterface_impl() { m_runtime->UnRegisterContext(m_cx); { JSAutoRequest rq(m_cx); JS_LeaveCompartment(m_cx, m_comp); } JS_DestroyContext(m_cx); } void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) { JSAutoRequest rq(m_cx); JS::RootedObject nativeScope(m_cx, m_nativeScope); JS::RootedFunction func(m_cx, JS_DefineFunction(m_cx, nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); } ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& runtime) : m(new ScriptInterface_impl(nativeScopeName, runtime)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } m_CxPrivate.pScriptInterface = this; JS_SetContextPrivate(m->m_cx, (void*)&m_CxPrivate); } ScriptInterface::~ScriptInterface() { if (ThreadUtil::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::ShutDown() { JS_ShutDown(); } void ScriptInterface::SetCallbackData(void* pCBData) { m_CxPrivate.pCBData = pCBData; } ScriptInterface::CxPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CxPrivate* pCxPrivate = (CxPrivate*)JS_GetContextPrivate(cx); return pCxPrivate; } JS::Value ScriptInterface::GetCachedValue(CACHED_VAL valueIdentifier) { std::map>::iterator it = m->m_ScriptValCache.find(valueIdentifier); ENSURE(it != m->m_ScriptValCache.end()); return it->second.get(); } bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } JSAutoRequest rq(m->m_cx); JS::RootedValue proto(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); if (JS_GetProperty(m->m_cx, global, "Vector2Dprototype", &proto)) m->m_ScriptValCache[CACHE_VECTOR2DPROTO] = DefPersistentRooted(GetJSRuntime(), proto); if (JS_GetProperty(m->m_cx, global, "Vector3Dprototype", &proto)) m->m_ScriptValCache[CACHE_VECTOR3DPROTO] = DefPersistentRooted(GetJSRuntime(), proto); return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { JSAutoRequest rq(m->m_cx); JS::RootedValue math(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); if (JS_GetProperty(m->m_cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(m->m_cx, &math.toObject()); JS::RootedFunction random(m->m_cx, JS_DefineFunction(m->m_cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) { m->Register(name, fptr, (uint)nargs); } JSContext* ScriptInterface::GetContext() const { return m->m_cx; } JSRuntime* ScriptInterface::GetJSRuntime() const { return m->m_runtime->m_rt; } shared_ptr ScriptInterface::GetRuntime() const { return m->m_runtime; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) { JSAutoRequest rq(m->m_cx); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(m->m_cx, &ctor.toObject()); out.setObjectOrNull(JS_New(m->m_cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { JSAutoRequest rq(m->m_cx); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(m->m_cx, m->m_glob); JS::RootedObject obj(m->m_cx, JS_InitClass(m->m_cx, global, JS::NullPtr(), clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == NULL) throw PSERROR_Scripting_DefineType_CreationFailed(); CustomType type; type.m_Prototype = DefPersistentRooted(m->m_cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; m_CustomObjectTypes[typeName] = std::move(type); } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); JS::RootedObject prototype(m->m_cx, it->second.m_Prototype.get()); return JS_NewObject(m->m_cx, (*it).second.m_Class, prototype, JS::NullPtr()); } bool ScriptInterface::CallFunctionVoid(JS::HandleValue val, const char* name) { JSAutoRequest rq(m->m_cx); JS::RootedValue jsRet(m->m_cx); return CallFunction_(val, name, JS::HandleValueArray::empty(), &jsRet); } bool ScriptInterface::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) { JSAutoRequest rq(m->m_cx); JS::RootedObject obj(m->m_cx); if (!JS_ValueToObject(m->m_cx, val, &obj) || !obj) return false; // Check that the named function actually exists, to avoid ugly JS error reports // when calling an undefined value bool found; if (!JS_HasProperty(m->m_cx, obj, name, &found) || !found) return false; bool ok = JS_CallFunctionName(m->m_cx, obj, name, argv, ret); return ok; } jsval ScriptInterface::GetGlobalObject() { JSAutoRequest rq(m->m_cx); return JS::ObjectValue(*JS::CurrentGlobalOrNull(m->m_cx)); } JSClass* ScriptInterface::GetGlobalClass() { return &global_class; } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); if (!replace) { bool found; if (!JS_HasProperty(m->m_cx, global, name, &found)) return false; if (found) { JS_ReportError(m->m_cx, "SetGlobal \"%s\" called multiple times", name); return false; } } bool ok = JS_DefineProperty(m->m_cx, global, name, value, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); return ok; } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) { JSAutoRequest rq(m->m_cx); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); if (! JS_DefineProperty(m->m_cx, object, name, value, attrs)) return false; return true; } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) { JSAutoRequest rq(m->m_cx); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); if (!JS_DefineUCProperty(m->m_cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, NULL, NULL, attrs)) return false; return true; } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) { JSAutoRequest rq(m->m_cx); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); if (! JS_DefinePropertyById(m->m_cx, object, INT_TO_JSID(name), value, NULL, NULL, attrs)) return false; return true; } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) { return GetProperty_(obj, name, out); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) { JSContext* cx = GetContext(); JSAutoRequest rq(cx); JS::RootedValue val(cx); if (!GetProperty_(obj, name, &val)) return false; if (!val.isObject()) { LOGERROR("GetProperty failed: trying to get an object, but the property is not an object!"); return false; } out.set(&val.toObject()); return true; } bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) { return GetPropertyInt_(obj, name, out); } bool ScriptInterface::GetProperty_(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) { JSAutoRequest rq(m->m_cx); if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); if (!JS_GetProperty(m->m_cx, object, name, out)) return false; return true; } bool ScriptInterface::GetPropertyInt_(JS::HandleValue obj, int name, JS::MutableHandleValue out) { JSAutoRequest rq(m->m_cx); JS::RootedId nameId(m->m_cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); if (!JS_GetPropertyById(m->m_cx, object, nameId, out)) return false; return true; } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) { // TODO: proper errorhandling JSAutoRequest rq(m->m_cx); if (!obj.isObject()) return false; JS::RootedObject object(m->m_cx, &obj.toObject()); bool found; if (!JS_HasProperty(m->m_cx, object, name, &found)) return false; return found; } bool ScriptInterface::EnumeratePropertyNamesWithPrefix(JS::HandleValue objVal, const char* prefix, std::vector& out) { JSAutoRequest rq(m->m_cx); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNamesWithPrefix expected object type!"); return false; } if(objVal.isNull()) return true; // reached the end of the prototype chain JS::RootedObject obj(m->m_cx, &objVal.toObject()); JS::AutoIdArray props(m->m_cx, JS_Enumerate(m->m_cx, obj)); if (!props) return false; for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(m->m_cx, props[i]); JS::RootedValue val(m->m_cx); if (!JS_IdToValue(m->m_cx, id, &val)) return false; if (!val.isString()) continue; // ignore integer properties JS::RootedString name(m->m_cx, val.toString()); size_t len = strlen(prefix)+1; std::vector buf(len); size_t prefixLen = strlen(prefix) * sizeof(char); JS_EncodeStringToBuffer(m->m_cx, name, &buf[0], prefixLen); buf[len-1]= '\0'; if(0 == strcmp(&buf[0], prefix)) { size_t len; const char16_t* chars = JS_GetStringCharsAndLength(m->m_cx, name, &len); out.push_back(std::string(chars, chars+len)); } } // Recurse up the prototype chain JS::RootedObject prototype(m->m_cx); if (JS_GetPrototype(m->m_cx, obj, &prototype)) { JS::RootedValue prototypeVal(m->m_cx, JS::ObjectOrNullValue(prototype)); if (! EnumeratePropertyNamesWithPrefix(prototypeVal, prefix, out)) return false; } return true; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { JSAutoRequest rq(m->m_cx); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(m->m_cx, &objVal.toObject()); JS::RootedObject proto(m->m_cx, &protoVal.toObject()); return JS_SetPrototype(m->m_cx, obj, proto); } bool ScriptInterface::FreezeObject(JS::HandleValue objVal, bool deep) { JSAutoRequest rq(m->m_cx); if (!objVal.isObject()) return false; JS::RootedObject obj(m->m_cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(m->m_cx, obj); else return JS_FreezeObject(m->m_cx, obj); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); utf16string codeUtf16(code.begin(), code.end()); uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(m->m_cx); options.setFileAndLine(filenameStr.c_str(), lineNo); options.setCompileAndGo(true); JS::RootedFunction func(m->m_cx, JS_CompileUCFunction(m->m_cx, global, NULL, 0, NULL, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), options) ); if (!func) return false; JS::RootedValue rval(m->m_cx); return JS_CallFunction(m->m_cx, JS::NullPtr(), func, JS::HandleValueArray::empty(), &rval); } shared_ptr ScriptInterface::CreateRuntime(shared_ptr parentRuntime, int runtimeSize, int heapGrowthBytesGCTrigger) { return shared_ptr(new ScriptRuntime(parentRuntime, runtimeSize, heapGrowthBytesGCTrigger)); } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::wstring& code) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); utf16string codeUtf16(code.begin(), code.end()); uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(m->m_cx); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); return JS::Evaluate(m->m_cx, global, opts, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &rval); } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } std::wstring code = wstring_from_utf8(file.DecodeUTF8()); // assume it's UTF-8 utf16string codeUtf16(code.begin(), code.end()); uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(m->m_cx); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); return JS::Evaluate(m->m_cx, global, opts, reinterpret_cast(codeUtf16.c_str()), (uint)(codeUtf16.length()), &rval); } bool ScriptInterface::Eval(const char* code) { JSAutoRequest rq(m->m_cx); JS::RootedValue rval(m->m_cx); return Eval_(code, &rval); } bool ScriptInterface::Eval_(const char* code, JS::MutableHandleValue rval) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); utf16string codeUtf16(code, code+strlen(code)); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine("(eval)", 1); return JS::Evaluate(m->m_cx, global, opts, reinterpret_cast(codeUtf16.c_str()), (uint)codeUtf16.length(), rval); } bool ScriptInterface::Eval_(const wchar_t* code, JS::MutableHandleValue rval) { JSAutoRequest rq(m->m_cx); JS::RootedObject global(m->m_cx, m->m_glob); utf16string codeUtf16(code, code+wcslen(code)); JS::CompileOptions opts(m->m_cx); opts.setFileAndLine("(eval)", 1); return JS::Evaluate(m->m_cx, global, opts, reinterpret_cast(codeUtf16.c_str()), (uint)codeUtf16.length(), rval); } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) { JSAutoRequest rq(m->m_cx); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(m->m_cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; LOGERROR("JS_ParseJSON failed!"); if (!JS_IsExceptionPending(m->m_cx)) return false; JS::RootedValue exc(m->m_cx); if (!JS_GetPendingException(m->m_cx, &exc)) return false; JS_ClearPendingException(m->m_cx); // We expect an object of type SyntaxError if (!exc.isObject()) return false; JS::RootedValue rval(m->m_cx); JS::RootedObject excObj(m->m_cx, &exc.toObject()); if (!JS_CallFunctionName(m->m_cx, excObj, "toString", JS::HandleValueArray::empty(), &rval)) return false; std::wstring error; ScriptInterface::FromJSVal(m->m_cx, rval, error); LOGERROR("%s", utf8_from_wstring(error)); return false; } void ScriptInterface::ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return; } std::string content(file.DecodeUTF8()); // assume it's UTF-8 if (!ParseJSON(content, out)) LOGERROR("Failed to parse '%s'", path.string8()); } struct Stringifier { static bool callback(const char16_t* buf, u32 len, void* data) { utf16string str(buf, buf+len); std::wstring strw(str.begin(), str.end()); Status err; // ignore Unicode errors static_cast(data)->stream << utf8_from_wstring(strw, &err); return true; } std::stringstream stream; }; // TODO: It's not quite clear why JS_Stringify needs JS::MutableHandleValue. |obj| should not get modified. // It probably has historical reasons and could be changed by SpiderMonkey in the future. std::string ScriptInterface::StringifyJSON(JS::MutableHandleValue obj, bool indent) { JSAutoRequest rq(m->m_cx); Stringifier str; JS::RootedValue indentVal(m->m_cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(m->m_cx, obj, JS::NullPtr(), indentVal, &Stringifier::callback, &str)) { JS_ClearPendingException(m->m_cx); LOGERROR("StringifyJSON failed"); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) { JSAutoRequest rq(m->m_cx); if (obj.isUndefined()) return "(void 0)"; // Try to stringify as JSON if possible // (TODO: this is maybe a bad idea since it'll drop 'undefined' values silently) if (pretty) { Stringifier str; JS::RootedValue indentVal(m->m_cx, JS::Int32Value(2)); // Temporary disable the error reporter, so we don't print complaints about cyclic values JSErrorReporter er = JS_SetErrorReporter(m->m_cx, NULL); bool ok = JS_Stringify(m->m_cx, obj, JS::NullPtr(), indentVal, &Stringifier::callback, &str); // Restore error reporter JS_SetErrorReporter(m->m_cx, er); if (ok) return str.stream.str(); // Clear the exception set when Stringify failed JS_ClearPendingException(m->m_cx); } // Caller didn't want pretty output, or JSON conversion failed (e.g. due to cycles), // so fall back to obj.toSource() std::wstring source = L"(error)"; CallFunction(obj, "toSource", source); return utf8_from_wstring(source); } void ScriptInterface::ReportError(const char* msg) { JSAutoRequest rq(m->m_cx); // JS_ReportError by itself doesn't seem to set a JS-style exception, and so // script callers will be unable to catch anything. So use JS_SetPendingException // to make sure there really is a script-level exception. But just set it to undefined // because there's not much value yet in throwing a real exception object. JS_SetPendingException(m->m_cx, JS::UndefinedHandleValue); // And report the actual error JS_ReportError(m->m_cx, "%s", msg); // TODO: Why doesn't JS_ReportPendingException(m->m_cx); work? } bool ScriptInterface::IsExceptionPending(JSContext* cx) { JSAutoRequest rq(cx); return JS_IsExceptionPending(cx) ? true : false; } const JSClass* ScriptInterface::GetClass(JS::HandleObject obj) { return JS_GetClass(obj); } void* ScriptInterface::GetPrivate(JS::HandleObject obj) { // TODO: use JS_GetInstancePrivate return JS_GetPrivate(obj); } void ScriptInterface::MaybeGC() { JS_MaybeGC(m->m_cx); } void ScriptInterface::ForceGC() { PROFILE2("JS_GC"); JS_GC(this->GetJSRuntime()); } JS::Value ScriptInterface::CloneValueFromOtherContext(ScriptInterface& otherContext, JS::HandleValue val) { PROFILE("CloneValueFromOtherContext"); JSAutoRequest rq(m->m_cx); JS::RootedValue out(m->m_cx); shared_ptr structuredClone = otherContext.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone::StructuredClone() : m_Data(NULL), m_Size(0) { } ScriptInterface::StructuredClone::~StructuredClone() { if (m_Data) JS_ClearStructuredClone(m_Data, m_Size, NULL, NULL); } shared_ptr ScriptInterface::WriteStructuredClone(JS::HandleValue v) { JSAutoRequest rq(m->m_cx); u64* data = NULL; size_t nbytes = 0; if (!JS_WriteStructuredClone(m->m_cx, v, &data, &nbytes, NULL, NULL, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); return shared_ptr(); } shared_ptr ret (new StructuredClone); ret->m_Data = data; ret->m_Size = nbytes; return ret; } void ScriptInterface::ReadStructuredClone(const shared_ptr& ptr, JS::MutableHandleValue ret) { JSAutoRequest rq(m->m_cx); JS_ReadStructuredClone(m->m_cx, ptr->m_Data, ptr->m_Size, JS_STRUCTURED_CLONE_VERSION, ret, NULL, NULL); } Index: ps/trunk/source/scriptinterface/ScriptRuntime.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptRuntime.cpp (revision 18422) +++ ps/trunk/source/scriptinterface/ScriptRuntime.cpp (revision 18423) @@ -1,338 +1,338 @@ -/* 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 * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ScriptRuntime.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" void GCSliceCallbackHook(JSRuntime* UNUSED(rt), JS::GCProgress progress, const JS::GCDescription& UNUSED(desc)) { /* * During non-incremental GC, the GC is bracketed by JSGC_CYCLE_BEGIN/END * callbacks. During an incremental GC, the sequence of callbacks is as * follows: * JSGC_CYCLE_BEGIN, JSGC_SLICE_END (first slice) * JSGC_SLICE_BEGIN, JSGC_SLICE_END (second slice) * ... * JSGC_SLICE_BEGIN, JSGC_CYCLE_END (last slice) */ if (progress == JS::GC_SLICE_BEGIN) { if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Start("GCSlice"); g_Profiler2.RecordRegionEnter("GCSlice"); } else if (progress == JS::GC_SLICE_END) { if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Stop(); - g_Profiler2.RecordRegionLeave("GCSlice"); + g_Profiler2.RecordRegionLeave(); } else if (progress == JS::GC_CYCLE_BEGIN) { if (CProfileManager::IsInitialised() && ThreadUtil::IsMainThread()) g_Profiler.Start("GCSlice"); g_Profiler2.RecordRegionEnter("GCSlice"); } else if (progress == JS::GC_CYCLE_END) { 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 // Search for "Nonincremental reason" if there are problems running GC incrementally. #if 0 if (progress == JS::GCProgress::GC_CYCLE_BEGIN) printf("starting cycle ===========================================\n"); const char16_t* str = desc.formatMessage(rt); int len = 0; for(int i = 0; i < 10000; i++) { len++; if(!str[i]) break; } wchar_t outstring[len]; for(int i = 0; i < len; i++) { outstring[i] = (wchar_t)str[i]; } printf("---------------------------------------\n: %ls \n---------------------------------------\n", outstring); #endif } void ScriptRuntime::GCCallback(JSRuntime* UNUSED(rt), JSGCStatus status, void *data) { if (status == JSGC_END) reinterpret_cast(data)->GCCallbackMember(); } void ScriptRuntime::GCCallbackMember() { m_FinalizationListObjectIdCache.clear(); } void ScriptRuntime::AddDeferredFinalizationObject(const std::shared_ptr& obj) { m_FinalizationListObjectIdCache.push_back(obj); } bool ScriptRuntime::m_Initialized = false; ScriptRuntime::ScriptRuntime(shared_ptr parentRuntime, int runtimeSize, int heapGrowthBytesGCTrigger): m_LastGCBytes(0), m_LastGCCheck(0.0f), m_HeapGrowthBytesGCTrigger(heapGrowthBytesGCTrigger), m_RuntimeSize(runtimeSize) { if (!m_Initialized) { ENSURE(JS_Init()); m_Initialized = true; } JSRuntime* parentJSRuntime = parentRuntime ? parentRuntime->m_rt : nullptr; m_rt = JS_NewRuntime(runtimeSize, JS_USE_HELPER_THREADS, parentJSRuntime); ENSURE(m_rt); // TODO: error handling if (g_ScriptProfilingEnabled) { // Execute and call hooks are disabled if the runtime debug mode is disabled JS_SetRuntimeDebugMode(m_rt, true); // Profiler isn't thread-safe, so only enable this on the main thread if (ThreadUtil::IsMainThread()) { if (CProfileManager::IsInitialised()) { JS_SetExecuteHook(m_rt, jshook_script, this); JS_SetCallHook(m_rt, jshook_function, this); } } } JS::SetGCSliceCallback(m_rt, GCSliceCallbackHook); JS_SetGCCallback(m_rt, ScriptRuntime::GCCallback, this); JS_SetGCParameter(m_rt, JSGC_MAX_MALLOC_BYTES, m_RuntimeSize); JS_SetGCParameter(m_rt, JSGC_MAX_BYTES, m_RuntimeSize); JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_INCREMENTAL); // The whole heap-growth mechanism seems to work only for non-incremental GCs. // We disable it to make it more clear if full GCs happen triggered by this JSAPI internal mechanism. JS_SetGCParameter(m_rt, JSGC_DYNAMIC_HEAP_GROWTH, false); m_dummyContext = JS_NewContext(m_rt, STACK_CHUNK_SIZE); ENSURE(m_dummyContext); } ScriptRuntime::~ScriptRuntime() { JS_DestroyContext(m_dummyContext); JS_SetGCCallback(m_rt, nullptr, nullptr); JS_DestroyRuntime(m_rt); ENSURE(m_FinalizationListObjectIdCache.empty() && "Leak: Removing callback while some objects still aren't finalized!"); } void ScriptRuntime::RegisterContext(JSContext* cx) { m_Contexts.push_back(cx); } void ScriptRuntime::UnRegisterContext(JSContext* cx) { m_Contexts.remove(cx); } #define GC_DEBUG_PRINT 0 void ScriptRuntime::MaybeIncrementalGC(double delay) { PROFILE2("MaybeIncrementalGC"); if (JS::IsIncrementalGCEnabled(m_rt)) { // The idea is to get the heap size after a completed GC and trigger the next GC when the heap size has // reached m_LastGCBytes + X. // In practice it doesn't quite work like that. When the incremental marking is completed, the sweeping kicks in. // The sweeping actually frees memory and it does this in a background thread (if JS_USE_HELPER_THREADS is set). // While the sweeping is happening we already run scripts again and produce new garbage. const int GCSliceTimeBudget = 30; // Milliseconds an incremental slice is allowed to run // Have a minimum time in seconds to wait between GC slices and before starting a new GC to distribute the GC // load and to hopefully make it unnoticeable for the player. This value should be high enough to distribute // the load well enough and low enough to make sure we don't run out of memory before we can start with the // sweeping. if (timer_Time() - m_LastGCCheck < delay) return; m_LastGCCheck = timer_Time(); int gcBytes = JS_GetGCParameter(m_rt, JSGC_BYTES); #if GC_DEBUG_PRINT std::cout << "gcBytes: " << gcBytes / 1024 << " KB" << std::endl; #endif if (m_LastGCBytes > gcBytes || m_LastGCBytes == 0) { #if GC_DEBUG_PRINT printf("Setting m_LastGCBytes: %d KB \n", gcBytes / 1024); #endif m_LastGCBytes = gcBytes; } // Run an additional incremental GC slice if the currently running incremental GC isn't over yet // ... or // start a new incremental GC if the JS heap size has grown enough for a GC to make sense if (JS::IsIncrementalGCInProgress(m_rt) || (gcBytes - m_LastGCBytes > m_HeapGrowthBytesGCTrigger)) { #if GC_DEBUG_PRINT if (JS::IsIncrementalGCInProgress(m_rt)) printf("An incremental GC cycle is in progress. \n"); else printf("GC needed because JSGC_BYTES - m_LastGCBytes > m_HeapGrowthBytesGCTrigger \n" " JSGC_BYTES: %d KB \n m_LastGCBytes: %d KB \n m_HeapGrowthBytesGCTrigger: %d KB \n", gcBytes / 1024, m_LastGCBytes / 1024, m_HeapGrowthBytesGCTrigger / 1024); #endif // A hack to make sure we never exceed the runtime size because we can't collect the memory // fast enough. if(gcBytes > m_RuntimeSize / 2) { if (JS::IsIncrementalGCInProgress(m_rt)) { #if GC_DEBUG_PRINT printf("Finishing incremental GC because gcBytes > m_RuntimeSize / 2. \n"); #endif PrepareContextsForIncrementalGC(); JS::FinishIncrementalGC(m_rt, JS::gcreason::REFRESH_FRAME); } else { if (gcBytes > m_RuntimeSize * 0.75) { ShrinkingGC(); #if GC_DEBUG_PRINT printf("Running shrinking GC because gcBytes > m_RuntimeSize * 0.75. \n"); #endif } else { #if GC_DEBUG_PRINT printf("Running full GC because gcBytes > m_RuntimeSize / 2. \n"); #endif JS_GC(m_rt); } } } else { #if GC_DEBUG_PRINT if (!JS::IsIncrementalGCInProgress(m_rt)) printf("Starting incremental GC \n"); else printf("Running incremental GC slice \n"); #endif PrepareContextsForIncrementalGC(); JS::IncrementalGC(m_rt, JS::gcreason::REFRESH_FRAME, GCSliceTimeBudget); } m_LastGCBytes = gcBytes; } } } void ScriptRuntime::ShrinkingGC() { JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_COMPARTMENT); JS::PrepareForFullGC(m_rt); JS::ShrinkingGC(m_rt, JS::gcreason::REFRESH_FRAME); JS_SetGCParameter(m_rt, JSGC_MODE, JSGC_MODE_INCREMENTAL); } void* ScriptRuntime::jshook_script(JSContext* UNUSED(cx), JSAbstractFramePtr UNUSED(fp), bool UNUSED(isConstructing), bool before, bool* UNUSED(ok), void* closure) { if (before) g_Profiler.StartScript("script invocation"); else g_Profiler.Stop(); return closure; } void* ScriptRuntime::jshook_function(JSContext* cx, JSAbstractFramePtr fp, bool UNUSED(isConstructing), bool before, bool* UNUSED(ok), void* closure) { JSAutoRequest rq(cx); if (!before) { g_Profiler.Stop(); return closure; } JS::RootedFunction fn(cx, fp.maybeFun()); if (!fn) { g_Profiler.StartScript("(function)"); return closure; } // Try to get the name of non-anonymous functions JS::RootedString name(cx, JS_GetFunctionId(fn)); if (name) { char* chars = JS_EncodeString(cx, name); if (chars) { g_Profiler.StartScript(StringFlyweight(chars).get().c_str()); JS_free(cx, chars); return closure; } } // No name - use fileName and line instead JS::AutoFilename fileName; unsigned lineno; JS::DescribeScriptedCaller(cx, &fileName, &lineno); std::stringstream ss; ss << "(" << fileName.get() << ":" << lineno << ")"; g_Profiler.StartScript(StringFlyweight(ss.str()).get().c_str()); return closure; } void ScriptRuntime::PrepareContextsForIncrementalGC() { for (JSContext* const& ctx : m_Contexts) JS::PrepareZoneForGC(js::GetCompartmentZone(js::GetContextCompartment(ctx))); } Index: ps/trunk/source/tools/profiler2/Profiler2Report.js =================================================================== --- ps/trunk/source/tools/profiler2/Profiler2Report.js (nonexistent) +++ ps/trunk/source/tools/profiler2/Profiler2Report.js (revision 18423) @@ -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; +}; Index: ps/trunk/source/tools/profiler2/ReportDraw.js =================================================================== --- ps/trunk/source/tools/profiler2/ReportDraw.js (nonexistent) +++ ps/trunk/source/tools/profiler2/ReportDraw.js (revision 18423) @@ -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 Index: ps/trunk/source/tools/profiler2/profiler2.html =================================================================== --- ps/trunk/source/tools/profiler2/profiler2.html (revision 18422) +++ ps/trunk/source/tools/profiler2/profiler2.html (revision 18423) @@ -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.

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

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

+
+
+ -

\ No newline at end of file
+

+
+
\ No newline at end of file
Index: ps/trunk/source/tools/profiler2/profiler2.js
===================================================================
--- ps/trunk/source/tools/profiler2/profiler2.js	(revision 18422)
+++ ps/trunk/source/tools/profiler2/profiler2.js	(revision 18423)
@@ -1,849 +1,497 @@
-// TODO: this code needs a load of cleaning up and documenting,
-// and feature additions and general improvement and unrubbishing
-
-// Item types returned by the engine
-var ITEM_EVENT = 1;
-var ITEM_ENTER = 2;
-var ITEM_LEAVE = 3;
-var ITEM_ATTRIBUTE = 4;
+// 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.
+
+// 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 = [];
+
+var g_main_thread = 0;
+var g_current_report = 0;
+
+var g_profile_path = null;
+var g_active_elements = [];
+var g_loading_timeout = null;
 
+function save_as_file()
+{
+    $.ajax({
+        url: 'http://127.0.0.1:8000/download',
+        success: function () {
+        },
+        error: function (jqXHR, textStatus, errorThrown) {
+        }
+    });
+}
 
-function hslToRgb(h, s, l, a)
+function get_history_data(report, thread, type)
 {
-    var r, g, b;
+    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;
 
-    if (s == 0)
+    let data = report_data.intervals_by_type_frame[type];
+    if (!data)
+        return ret;
+
+    let max = 0;
+    let avg = [];
+    let current_frame = 0;
+    for (let i = 0; i < data.length; i++)
     {
-        r = g = b = l;
+        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;
     }
-    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);
-    }
+    // 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)];
 
-    return 'rgba(' + Math.floor(r * 255) + ',' + Math.floor(g * 255) + ',' + Math.floor(b * 255) + ',' + a + ')';
+    if (ret.max > avg * 3)
+        ret.log_scale = true;
+
+    return ret;
 }
 
-function new_colour(id)
+function draw_frequency_graph()
 {
-    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);
-}
+    let canvas = document.getElementById("canvas_frequency");
+    canvas._tooltips = [];
 
-var g_used_colours = {};
+    let context = canvas.getContext("2d");
+    context.clearRect(0, 0, canvas.width, canvas.height);
 
+    let legend = document.getElementById("frequency_graph").querySelector("aside");
+    legend.innerHTML = "";
 
-var g_data;
+    if (!g_active_elements.length)
+        return;
 
-function refresh()
-{
-    if (1)
-        refresh_live();
-    else
-        refresh_jsonp('../../../binaries/system/profile2.jsonp');
-}
+    var series_data = {};
+    var use_log_scale = null;
 
-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;
-}
+    var x_scale = 0;
+    var y_scale = 0;
+    var padding = 10;
 
-function refresh_jsonp(url)
-{
-    var script = document.createElement('script');
-    
-    window.profileDataCB = function(data)
-    {
-        script.parentNode.removeChild(script);
+    var item_nb = 0;
 
-        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 tooltip_helper = {};
 
-        var range = {'seconds': 0.05};
+    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;
+        }
+    }
+    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;
 
-        rebuild_canvases();
-        update_display(range);
-    };
+        let nb = document.createElement("p");
+        nb.style.borderColor = colour;
+        nb.textContent = type + " - n=" + time_by_frame.length;
+        legend.appendChild(nb);
 
-    script.src = url;
-    document.body.appendChild(script);
-}
+        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;
 
-function refresh_live()
-{
-    $.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);
-            });
-        },
-        error: function (jqXHR, textStatus, errorThrown) {
-            alert('Failed to connect to server ("'+textStatus+'")');
-        }
-    });
-}
+            let y = time_by_frame[i]/y_scale;
+            if (use_log_scale)
+                y = Math.log10(1 + time_by_frame[i]/y_scale * 9);
 
-function refresh_thread(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_data = { 'threads': callback_data.threads };
+            context.globalCompositeOperation = "lighter";
 
-                //var range = {'numframes': 5};
-                var range = {'seconds': 0.05};
+            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();
 
-                rebuild_canvases();
-                update_display(range);
-            }
-        },
-        error: function (jqXHR, textStatus, errorThrown) {
-            alert('Failed to connect to server ("'+textStatus+'")');
+            last_val = y;
+            if (!tooltip_helper[Math.floor(x0)])
+                tooltip_helper[Math.floor(x0)] = [];
+            tooltip_helper[Math.floor(x0)].push([y, type]);
         }
-    });
-}
+        id++;
+    }
 
-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);
+    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); - $('#timelines').empty(); - $('#timelines').append(g_data.canvas_frames); - g_data.threads.forEach(function(thread) { - $('#timelines').append($(thread.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 ); }); - $('#timelines').append(g_data.canvas_zoom); - $('#timelines').append(g_data.text_output); } -function update_display(range) +function draw_history_graph() { - $(g_data.text_output).empty(); + let canvas = document.getElementById("canvas_history"); + canvas._tooltips = []; - var main_events = g_data.threads[0].data.events; + let context = canvas.getContext("2d"); + context.clearRect(0, 0, canvas.width, canvas.height); - var processed_main = g_data.threads[0].processed_events = compute_intervals(main_events, range); + let legend = document.getElementById("history_graph").querySelector("aside"); + legend.innerHTML = ""; -// display_top_items(main_events, g_data.text_output); + if (!g_active_elements.length) + return; - display_frames(processed_main, g_data.canvas_frames); - display_events(processed_main, g_data.canvas_frames); + var series_data = {}; + var use_log_scale = null; - $(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); + var frames_nb = Infinity; + var x_scale = 0; + var y_scale = 0; - g_data.threads.slice(1).forEach(function(thread) { - var processed_data = compute_intervals(thread.data.events, {'tmin': processed_main.tmin, 'tmax': processed_main.tmax}); + var item_nb = 0; - $(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); - }); - } + var tooltip_helper = {}; -function display_top_items(data, output) -{ - var items = {}; - for (var i = 0; i < data.length; ++i) + for (let typeI in g_active_elements) { - 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') + for (let rep in g_reports) { - 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; - } + 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; } - - tmin = data[start][1]; - tmax = data[end][1]; } - else if (range.seconds) + if (use_log_scale) { - 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; - } + let legend_item = document.createElement("p"); + legend_item.style.borderColor = "transparent"; + legend_item.textContent = " -- log y scale -- "; + legend.appendChild(legend_item); } - else + canvas.width = Math.max(frames_nb,600); + x_scale = frames_nb / canvas.width; + let id = 0; + for (let type in series_data) { - start = 0; - end = data.length - 1; - tmin = range.tmin; - tmax = range.tmax; + let colour = graph_colour(id); - 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; - } - } + let legend_item = document.createElement("p"); + legend_item.style.borderColor = colour; + legend_item.textContent = type; + legend.appendChild(legend_item); - for (var i = end; i > 0; --i) + let time_by_frame = series_data[type]; + let last_val = 0; + for (var i = 0; i < frames_nb; i++) { - var type = data[i][0]; - if ((type == ITEM_EVENT || type == ITEM_ENTER || type == ITEM_LEAVE) && data[i][1] < tmin) - break; - start = i; - } + let smoothed_time = time_by_frame[i];//smooth_1D(time_by_frame.slice(0), i, 3); - // 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; - } - } + let y = smoothed_time/y_scale; + if (use_log_scale) + y = Math.log10(1 + smoothed_time/y_scale * 9); - 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 (item_nb === 1) + { + context.beginPath(); + context.fillStyle = colour; + context.fillRect(i/x_scale,canvas.height,1/x_scale,-y*canvas.height); + } + else { - if (!events[events.length-1].attrs) - events[events.length-1].attrs = []; - events[events.length-1].attrs.push(data[i][1]); + 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]); } - else - { - lastWasEvent = false; - } + id++; } - - - 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]); - } - } + 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': +i, 'x1': +i+1, + 'y0': 0, 'y1': canvas.height, + 'text': function(text) { return function() { return text; } }(text) + }); } + set_tooltip_handlers(canvas); - return { 'frames': frames, 'events': events, 'intervals': intervals, 'tmin': tmin, 'tmax': tmax }; -} + [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); -function time_label(t) -{ - if (t > 1e-3) - return (t * 1e3).toFixed(2) + 'ms'; - else - return (t * 1e6).toFixed(2) + 'us'; + 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 display_frames(data, canvas) +function compare_reports() { - 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.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; - } - }; - -// var y_per_second = 1000; - var y_per_second = 100; + let section = document.getElementById("comparison"); + section.innerHTML = "

Report Comparison

"; - [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; - - 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 = '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(); -} + if (g_reports.length < 2) + { + section.innerHTML += "

Too few reports loaded

"; + return; + } -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) + if (g_active_elements.length != 1) { - 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) - }); + section.innerHTML += "

Too many of too few elements selected

"; + return; } - - ctx.restore(); -} -function display_hierarchy(main_data, data, canvas, range, zoom) -{ - canvas._tooltips = []; + 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; - 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 (frames_nb != g_reports[0].data().threads[g_main_thread].frames.length) + section.innerHTML += "

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

"; - if (range.tmin) - { - tmin = range.tmin; - tmax = range.tmax; - } - else - { - tmin = data.tmin; - tmax = data.tmax; - } + let reports_data = []; - canvas._hierarchyData = { 'range': range, 'tmin': tmin, 'tmax': tmax }; - - function time_to_x(t) + for (let rep in g_reports) { - return xpadding + (t - tmin) / (tmax - tmin) * width; + 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)}); } - - function x_to_time(x) + + let table_output = "" + for (let rep in reports_data) { - return tmin + (x - xpadding) * (tmax - tmin) / width; + 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) + "
"; +} - 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(); +function recompute_choices(report, thread) +{ + var choices = document.getElementById("choices").querySelector("nav"); + choices.innerHTML = "

Choices

"; + var types = {}; + var data = report.data().threads[thread]; - var BAR_SPACING = 16; + for (let i = 0; i < data.intervals.length; i++) + types[data.intervals[i].id] = 0; - for (var i = 0; i < data.intervals.length; ++i) - { - var interval = data.intervals[i]; - - if (interval.tmax <= tmin || interval.tmin > tmax) - continue; + var sorted_keys = Object.keys(types).sort(); - var label = interval.id; - if (interval.attrs) + 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 (/^\d+$/.exec(interval.attrs[0])) - label += ' ' + interval.attrs[0]; - else - label += ' [...]'; + 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(); } - 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)); - - 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(); + update_analysis(); } -function set_frames_zoom_handlers(canvas0) +function update_analysis() { - function do_zoom(event) - { - var zdata = canvas0._zoomData; + compare_reports(); + draw_history_graph(); + draw_frequency_graph(); +} + +function load_report_from_file(evt) +{ + var file = evt.target.files[0]; + if (!file) + return; + load_report(false, file); + evt.target.value = null; +} - 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; +function load_report(trylive, file) +{ + if (g_loading_timeout != undefined) + return; - 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); - } + 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); - 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); - }); + g_reports.push(Profiler2Report(on_report_loaded, trylive, file)); + g_loading_timeout = setTimeout(function() { on_report_loaded(false); }, 5000); } - -function set_zoom_handlers(main_data, data, canvas0, canvas1) + +function on_report_loaded(success) { - function do_zoom(event) + let element = document.getElementById("Report" + (g_reports.length-1)); + let report = g_reports[g_reports.length-1]; + if (!success) { - 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); + 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; } - - 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); - }); + clearTimeout(g_loading_timeout); + g_loading_timeout = null; + select_report(+element.dataset.id); + element.onclick = function() { select_report(+element.dataset.id);}; } -function set_tooltip_handlers(canvas) +function select_report(id) { - 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_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}); - 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); - }); + recompute_choices(g_reports[id], g_main_thread); } -function search_regions(query) +window.onload = function() { - var re = new RegExp(query); - var data = g_data.threads[0].processed_events; - - var found = []; - for (var i = 0; i < data.intervals.length; ++i) - { - 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) + '')); - } + // 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 Index: ps/trunk/source/tools/profiler2/utilities.js =================================================================== --- ps/trunk/source/tools/profiler2/utilities.js (nonexistent) +++ ps/trunk/source/tools/profiler2/utilities.js (revision 18423) @@ -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