Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg (revision 24339) +++ ps/trunk/binaries/data/config/default.cfg (revision 24340) @@ -1,530 +1,535 @@ ; Global Configuration Settings ; ; ************************************************************** ; * DO NOT EDIT THIS FILE if you want personal customisations: * ; * create a text file called "local.cfg" instead, and copy * ; * the lines from this file that you want to change. * ; * * ; * If a setting is part of a section (for instance [hotkey]) * ; * you need to append the section name at the beginning of * ; * your custom line (for instance you need to write * ; * "hotkey.pause = Space" if you want to change the pausing * ; * hotkey to the spacebar). * ; * * ; * On Linux, create: * ; * $XDG_CONFIG_HOME/0ad/config/local.cfg * ; * (Note: $XDG_CONFIG_HOME defaults to ~/.config) * ; * * ; * On OS X, create: * ; * ~/Library/Application\ Support/0ad/config/local.cfg * ; * * ; * On Windows, create: * ; * %appdata%\0ad\config\local.cfg * ; * * ; ************************************************************** ; Enable/disable windowed mode by default. (Use Alt+Enter to toggle in the game.) windowed = false ; Show detailed tooltips (Unit stats) showdetailedtooltips = false ; Pause the game on window focus loss (Only applicable to single player mode) pauseonfocusloss = true ; Persist settings after leaving the game setup screen persistmatchsettings = true ; Default player name to use in multiplayer ; playername = "anonymous" ; Default server name or IP to use in multiplayer multiplayerserver = "127.0.0.1" ; Force a particular resolution. (If these are 0, the default is ; to keep the current desktop resolution in fullscreen mode or to ; use 1024x768 in windowed mode.) xres = 0 yres = 0 ; Force a non-standard bit depth (if 0 then use the current desktop bit depth) bpp = 0 ; Preferred display (for multidisplay setups, only works with SDL 2.0) display = 0 ; Emulate right-click with Ctrl+Click on Mac mice macmouse = false ; System settings: ; if false, actors won't be rendered but anything entity will be. renderactors = true watereffects=true ; When disabled, force usage of the fixed pipeline water. This is faster, but really, really ugly. waterfancyeffects = false waterrealdepth = true waterrefraction = true waterreflection = true shadowsonwater = false shadows = true shadowquality = 0 ; Shadow map resolution. (-2 - Very Low, -1 - Low, 0 - Medium, 1 - High, 2 - Very High) ; High values can crash the game when using a graphics card with low memory! shadowpcf = true shadowsfixed = false ; When enabled shadows are rendered only on the shadowsfixeddistance = 300.0 ; fixed distance and without swimming effect. vsync = false particles = true fog = true silhouettes = true showsky = true novbo = false ; Disable hardware cursors nohwcursor = false ; Specify the render path. This can be one of: ; default Automatically select one of the below, depending on system capabilities ; fixed Only use OpenGL fixed function pipeline ; shader Use vertex/fragment shaders for transform and lighting where possible ; Using 'fixed' instead of 'default' may work around some graphics-related problems, ; but will reduce performance and features when a modern graphics card is available. renderpath = default ;;;;; EXPERIMENTAL ;;;;; ; Prefer GLSL shaders over ARB shaders. Allows fancier graphical effects. preferglsl = false ; Experimental probably-non-working GPU skinning support; requires preferglsl; use at own risk gpuskinning = false ; Use smooth LOS interpolation smoothlos = false ; Use screen-space postprocessing filters (HDR, bloom, DOF, etc). Incompatible with fixed renderpath. postproc = false ; Use anti-aliasing techniques. antialiasing = "disabled" ; Use sharpening techniques. sharpening = "disabled" sharpness = 0.3 ; Quality level of shader effects (set to 10 to display all effects) materialmgr.quality = 2.0 ; Maximum distance to display parallax effect. Set to 0 to disable parallax. materialmgr.PARALLAX_DIST.max = 150 ; Maximum distance to display high quality parallax effect. materialmgr.PARALLAX_HQ_DIST.max = 75 ; Maximum distance to display very high quality parallax effect. Set to 30 to enable. materialmgr.PARALLAX_VHQ_DIST.max = 0 ;;;;;;;;;;;;;;;;;;;;;;;; ; Replace alpha-blending with alpha-testing, for performance experiments forcealphatest = false ; Color of the sky (in "r g b" format) skycolor = "0 0 0" [adaptivefps] session = 60 ; Throttle FPS in running games (prevents 100% CPU workload). menu = 60 ; Throttle FPS in menus only. +[profiler2] +server = "127.0.0.1" +server.port = "8000" ; Use a free port on your machine. +server.threads = "6" ; Enough for the browser's parallel connection limit + [hotkey] ; Each one of the specified keys will trigger the action on the left ; for multiple-key combinations, separate keys with '+'. ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS exit = "Ctrl+Break", "Super+Q", "Alt+F4" ; Exit to desktop cancel = Escape ; Close or cancel the current dialog box/popup confirm = Return ; Confirm the current command pause = Pause ; Pause/unpause game screenshot = F2 ; Take PNG screenshot bigscreenshot = "Shift+F2" ; Take large BMP screenshot togglefullscreen = "Alt+Return" ; Toggle fullscreen/windowed mode screenshot.watermark = "Alt+K" ; Toggle product/company watermark for official screenshots wireframe = "Alt+Shift+W" ; Toggle wireframe mode silhouettes = "Alt+Shift+S" ; Toggle unit silhouettes showsky = "Alt+Z" ; Toggle sky ; > DIALOG HOTKEYS summary = "Ctrl+Tab" ; Toggle in-game summary lobby = "Alt+L" ; Show the multiplayer lobby in a dialog window. structree = "Alt+Shift+T" ; Show structure tree civinfo = "Alt+Shift+H" ; Show civilization info ; > CLIPBOARD CONTROLS copy = "Ctrl+C" ; Copy to clipboard paste = "Ctrl+V" ; Paste from clipboard cut = "Ctrl+X" ; Cut selected text and copy to the clipboard ; > CONSOLE SETTINGS console.toggle = BackQuote, F9 ; Open/close console ; > OVERLAY KEYS fps.toggle = "Alt+F" ; Toggle frame counter realtime.toggle = "Alt+T" ; Toggle current display of computer time timeelapsedcounter.toggle = "F12" ; Toggle time elapsed counter ceasefirecounter.toggle = unused ; Toggle ceasefire counter ; > HOTKEYS ONLY chat = Return ; Toggle chat window teamchat = "T" ; Toggle chat window in team chat mode privatechat = "L" ; Toggle chat window and select the previous private chat partner ; > QUICKSAVE quicksave = "Shift+F5" quickload = "Shift+F8" [hotkey.camera] reset = "R" ; Reset camera rotation to default. follow = "F" ; Follow the first unit in the selection rallypointfocus = unused ; Focus the camera on the rally point of the selected building zoom.in = Plus, NumPlus ; Zoom camera in (continuous control) zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards rotate.down = "Ctrl+DownArrow", "Ctrl+S" ; Rotate camera to look downwards rotate.cw = "Ctrl+LeftArrow", "Ctrl+A", Q ; Rotate camera clockwise around terrain rotate.ccw = "Ctrl+RightArrow", "Ctrl+D", E ; Rotate camera anticlockwise around terrain rotate.wheel.cw = "Shift+WheelUp", MouseX1 ; Rotate camera clockwise around terrain (stepped control) rotate.wheel.ccw = "Shift+WheelDown", MouseX2 ; Rotate camera anticlockwise around terrain (stepped control) pan = MouseMiddle ; Enable scrolling by moving mouse left = A, LeftArrow ; Scroll or rotate left right = D, RightArrow ; Scroll or rotate right up = W, UpArrow ; Scroll or rotate up/forwards down = S, DownArrow ; Scroll or rotate down/backwards scroll.speed.increase = "Ctrl+Shift+S" ; Increase scroll speed scroll.speed.decrease = "Ctrl+Alt+S" ; Decrease scroll speed rotate.speed.increase = "Ctrl+Shift+R" ; Increase rotation speed rotate.speed.decrease = "Ctrl+Alt+R" ; Decrease rotation speed zoom.speed.increase = "Ctrl+Shift+Z" ; Increase zoom speed zoom.speed.decrease = "Ctrl+Alt+Z" ; Decrease zoom speed [hotkey.camera.jump] 1 = F5 ; Jump to position N 2 = F6 3 = F7 4 = F8 ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.camera.jump.set] 1 = "Ctrl+F5" ; Set jump position N 2 = "Ctrl+F6" 3 = "Ctrl+F7" 4 = "Ctrl+F8" ;5 = ;6 = ;7 = ;8 = ;9 = ;10 = [hotkey.profile] toggle = "F11" ; Enable/disable real-time profiler save = "Shift+F11" ; Save current profiler data to logs/profile.txt [hotkey.profile2] toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler [hotkey.selection] cancel = Esc ; Un-select all units and cancel building placement add = Shift ; Add units to selection militaryonly = Alt ; Add only military units to the selection nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection idleonly = "I" ; Select only idle units woundedonly = "O" ; Select only wounded units remove = Ctrl ; Remove units from selection idleworker = Period, NumDecimal ; Select next idle worker idlewarrior = Slash, NumDivide ; Select next idle warrior idleunit = BackSlash ; Select next idle unit offscreen = Alt ; Include offscreen units in selection [hotkey.selection.group.add] 0 = "Shift+0", "Shift+Num0" 1 = "Shift+1", "Shift+Num1" 2 = "Shift+2", "Shift+Num2" 3 = "Shift+3", "Shift+Num3" 4 = "Shift+4", "Shift+Num4" 5 = "Shift+5", "Shift+Num5" 6 = "Shift+6", "Shift+Num6" 7 = "Shift+7", "Shift+Num7" 8 = "Shift+8", "Shift+Num8" 9 = "Shift+9", "Shift+Num9" [hotkey.selection.group.save] 0 = "Ctrl+0", "Ctrl+Num0" 1 = "Ctrl+1", "Ctrl+Num1" 2 = "Ctrl+2", "Ctrl+Num2" 3 = "Ctrl+3", "Ctrl+Num3" 4 = "Ctrl+4", "Ctrl+Num4" 5 = "Ctrl+5", "Ctrl+Num5" 6 = "Ctrl+6", "Ctrl+Num6" 7 = "Ctrl+7", "Ctrl+Num7" 8 = "Ctrl+8", "Ctrl+Num8" 9 = "Ctrl+9", "Ctrl+Num9" [hotkey.selection.group.select] 0 = 0, Num0 1 = 1, Num1 2 = 2, Num2 3 = 3, Num3 4 = 4, Num4 5 = 5, Num5 6 = 6, Num6 7 = 7, Num7 8 = 8, Num8 9 = 9, Num9 [hotkey.session] kill = Delete, Backspace ; Destroy selected units stop = "H" ; Stop the current action backtowork = "Y" ; The unit will go back to work unload = "U" ; Unload garrisoned units when a building/mechanical unit is selected move = unused ; Modifier to move to a point instead of another action (e.g. gather) attack = Ctrl ; Modifier to attack instead of another action (e.g. capture) attackmove = Ctrl ; Modifier to attackmove when clicking on a point attackmoveUnit = "Ctrl+Q" ; Modifier to attackmove targeting only units when clicking on a point (should contain the attackmove keys) garrison = Ctrl ; Modifier to garrison when clicking on building autorallypoint = Ctrl ; Modifier to set the rally point on the building itself guard = "G" ; Modifier to escort/guard when clicking on unit/building patrol = "P" ; Modifier to patrol a unit repair = "J" ; Modifier to repair when clicking on building/mechanical unit queue = Shift ; Modifier to queue unit orders instead of replacing orderone = Alt ; Modifier to order only one entity in selection. batchtrain = Shift ; Modifier to train units in batches massbarter = Shift ; Modifier to barter bunch of resources masstribute = Shift ; Modifier to tribute bunch of resources noconfirmation = Shift ; Do not ask confirmation when deleting a building/unit fulltradeswap = Shift ; Modifier to put the desired trade resource to 100% unloadtype = Shift ; Modifier to unload all units of type deselectgroup = Ctrl ; Modifier to deselect units when clicking group icon, instead of selecting rotate.cw = RightBracket ; Rotate building placement preview clockwise rotate.ccw = LeftBracket ; Rotate building placement preview anticlockwise snaptoedges = Ctrl ; Modifier to align new structures with nearby existing structure ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel highlightguarding = PageDown ; Toggle highlight of guarding units highlightguarded = PageUp ; Toggle highlight of guarded units diplomacycolors = "Alt+X" ; Toggle diplomacy colors toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures togglehealrange = "Alt+B" ; Toggle display of heal range overlays of selected units [hotkey.session.gui] toggle = "Alt+G" ; Toggle visibility of session GUI menu.toggle = "F10" ; Toggle in-game menu diplomacy.toggle = "Ctrl+H" ; Toggle in-game diplomacy page barter.toggle = "Ctrl+B" ; Toggle in-game barter/trade page objectives.toggle = "Ctrl+O" ; Toggle in-game objectives page tutorial.toggle = "Ctrl+P" ; Toggle in-game tutorial panel [hotkey.session.savedgames] delete = Delete, Backspace ; Delete the selected saved game asking confirmation noconfirmation = Shift ; Do not ask confirmation when deleting a game [hotkey.session.queueunit] ; > UNIT TRAINING 1 = "Z" ; add first unit type to queue 2 = "X" ; add second unit type to queue 3 = "C" ; add third unit type to queue 4 = "V" ; add fourth unit type to queue 5 = "B" ; add fivth unit type to queue 6 = "N" ; add sixth unit type to queue 7 = "M" ; add seventh unit type to queue 8 = Comma ; add eighth unit type to queue [hotkey.session.timewarp] fastforward = Space ; If timewarp mode enabled, speed up the game rewind = "Shift+Backspace" ; If timewarp mode enabled, go back to earlier point in the game [hotkey.tab] next = "Tab", "Alt+S" ; Show the next tab prev = "Shift+Tab", "Alt+W" ; Show the previous tab [hotkey.text] ; > GUI TEXTBOX HOTKEYS delete.left = "Ctrl+Backspace" ; Delete word to the left of cursor delete.right = "Ctrl+Del" ; Delete word to the right of cursor move.left = "Ctrl+LeftArrow" ; Move cursor to start of word to the left of cursor move.right = "Ctrl+RightArrow" ; Move cursor to start of word to the right of cursor [gui] cursorblinkrate = 0.5 ; Cursor blink rate in seconds (0.0 to disable blinking) scale = 1.0 ; GUI scaling factor, for improved compatibility with 4K displays [gui.gamesetup] enabletips = true ; Enable/Disable tips during gamesetup (for newcomers) assignplayers = everyone ; Whether to assign joining clients to free playerslots. Possible values: everyone, buddies, disabled. aidifficulty = 3 ; Difficulty level, from 0 (easiest) to 5 (hardest) aibehavior = "random" ; Default behavior of the AI (random, balanced, aggressive or defensive) settingsslide = true ; Enable/Disable settings panel slide [gui.loadingscreen] progressdescription = false ; Whether to display the progress percent or a textual description [gui.session] camerajump.threshold = 40 ; How close do we have to be to the actual location in order to jump back to the previous one? timeelapsedcounter = false ; Show the game duration in the top right corner ceasefirecounter = false ; Show the remaining ceasefire time in the top right corner batchtrainingsize = 5 ; Number of units to be trained per batch by default (when pressing the hotkey) scrollbatchratio = 1 ; Number of times you have to scroll to increase/decrease the batchsize by 1 woundedunithotkeythreshold = 33 ; The wounded unit hotkey considers the selected units as wounded if their health percentage falls below this number attackrange = true ; Display attack range overlays of selected defensive structures aurasrange = true ; Display aura range overlays of selected units and structures healrange = true ; Display heal range overlays of selected units rankabovestatusbar = true ; Show rank icons above status bars experiencestatusbar = true ; Show an experience status bar above each selected unit respoptooltipsort = 0 ; Sorting players in the resources and population tooltip by value (0 - no sort, -1 - ascending, 1 - descending) snaptoedges = "disabled" ; Possible values: disabled, enabled. snaptoedgesdistancethreshold = 15 ; On which distance we don't snap to edges disjointcontrolgroups = "true" ; Whether control groups are disjoint sets or entities can be in multiple control groups at the same time. [gui.session.minimap] blinkduration = 1.7 ; The blink duration while pinging pingduration = 50.0 ; The duration for which an entity will be pinged after an attack notification [gui.session.notifications] attack = true ; Show a chat notification if you are attacked by another player tribute = true ; Show a chat notification if an ally tributes resources to another team member if teams are locked, and all tributes in observer mode barter = true ; Show a chat notification to observers when a player bartered resources phase = completed ; Show a chat notification if you or an ally have started, aborted or completed a new phase, and phases of all players in observer mode. Possible values: none, completed, all. [gui.splashscreen] enable = true ; Enable/disable the splashscreen version = 0 ; Splashscreen version (date of last modification). By default, 0 to force splashscreen to appear at first launch [gui.session.diplomacycolors] self = "21 55 149" ; Color of your units when diplomacy colors are enabled ally = "86 180 31" ; Color of allies when diplomacy colors are enabled neutral = "231 200 5" ; Color of neutral players when diplomacy colors are enabled enemy = "150 20 20" ; Color of enemies when diplomacy colors are enabled [joystick] ; EXPERIMENTAL: joystick/gamepad settings enable = false deadzone = 8192 [joystick.camera] pan.x = 0 pan.y = 1 rotate.x = 3 rotate.y = 2 zoom.in = 5 zoom.out = 4 [chat] timestamp = true ; Show at which time chat messages have been sent [chat.session] extended = true ; Whether to display the chat history [lobby] history = 0 ; Number of past messages to display on join room = "arena24" ; Default MUC room to join server = "lobby.wildfiregames.com" ; Address of lobby server tls = true ; Whether to use TLS encryption when connecting to the server. verify_certificate = false ; Whether to reject connecting to the lobby if the TLS certificate is invalid (TODO: wait for Gloox GnuTLS trust implementation to be fixed) terms_url = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/prelobby/common/terms/"; Allows the user to save the text and print the terms terms_of_service = "0" ; Version (hash) of the Terms of Service that the user has accepted terms_of_use = "0" ; Version (hash) of the Terms of Use that the user has accepted privacy_policy = "0" ; Version (hash) of the Privacy Policy that the user has accepted xpartamupp = "wfgbot24" ; Name of the server-side XMPP-account that manage games echelon = "echelon24" ; Name of the server-side XMPP-account that manages ratings buddies = "," ; Comma separated list of playernames that the current user has marked as buddies rememberpassword = true ; Whether to store the encrypted password in the user config [lobby.columns] gamerating = false ; Show the average rating of the participating players in a column of the gamelist [lobby.stun] enabled = true ; The STUN protocol allows hosting games without configuring the firewall and router. ; If STUN is disabled, the game relies on direct connection, UPnP and port forwarding. server = "lobby.wildfiregames.com" ; Address of the STUN server. port = 3478 ; Port of the STUN server. delay = 200 ; Duration in milliseconds that is waited between STUN messages. ; Smaller numbers speed up joins but also become less stable. [mod] enabledmods = "mod public" [modio] public_key = "RWTsHxQMrRq4xwHisyBa2rNQfAedcINzbTT83jeX4/ZcfVxqLfWB4y8w" ; Public key corresponding to the private key valid mods are signed with disclaimer = "0" ; Version (hash) of the Disclaimer that the user has accepted [modio.v1] baseurl = "https://api.mod.io/v1" api_key = "23df258a71711ea6e4b50893acc1ba55" name_id = "0ad" [network] duplicateplayernames = false ; Rename joining player to "User (2)" if "User" is already connected, otherwise prohibit join. lateobservers = everyone ; Allow observers to join the game after it started. Possible values: everyone, buddies, disabled. observerlimit = 8 ; Prevent further observer joins in running games if this limit is reached gamestarttimeout = 60000 ; Don't disconnect clients timing out in the loading screen and rejoin process before exceeding this timeout. [overlay] fps = "false" ; Show frames per second in top right corner realtime = "false" ; Show current system time in top right corner netwarnings = "true" ; Show warnings if the network connection is bad [profiler2] autoenable = false ; Enable HTTP server output at startup (default off for security/performance) gpu.arb.enable = true ; Allow GL_ARB_timer_query timing mode when available gpu.ext.enable = true ; Allow GL_EXT_timer_query timing mode when available gpu.intel.enable = true ; Allow GL_INTEL_performance_queries timing mode when available [rlinterface] address = "127.0.0.1:6000" [sound] mastergain = 0.9 musicgain = 0.2 ambientgain = 0.6 actiongain = 0.7 uigain = 0.7 [sound.notify] nick = true ; Play a sound when someone mentions your name in the lobby or game gamesetup.join = false ; Play a sound when a new client joins the game setup [tinygettext] debug = false ; Print error messages each time a translation for an English string is not found. [userreport] ; Opt-in online user reporting system url_upload = "https://feedback.wildfiregames.com/report/upload/v1/" ; URL where UserReports are uploaded to url_publication = "https://feedback.wildfiregames.com/" ; URL where UserReports were analyzed and published url_terms = "https://trac.wildfiregames.com/browser/ps/trunk/binaries/data/mods/public/gui/userreport/Terms_and_Conditions.txt"; Allows the user to save the text and print the terms terms = "0" ; Version (hash) of the UserReporter Terms that the user has accepted [view] ; Camera control settings scroll.speed = 120.0 scroll.speed.modifier = 1.05 ; Multiplier for changing scroll speed rotate.x.speed = 1.2 rotate.x.min = 28.0 rotate.x.max = 60.0 rotate.x.default = 35.0 rotate.y.speed = 2.0 rotate.y.speed.wheel = 0.45 rotate.y.default = 0.0 rotate.speed.modifier = 1.05 ; Multiplier for changing rotation speed drag.speed = 0.5 zoom.speed = 256.0 zoom.speed.wheel = 32.0 zoom.min = 50.0 zoom.max = 200.0 zoom.default = 120.0 zoom.speed.modifier = 1.05 ; Multiplier for changing zoom speed pos.smoothness = 0.1 zoom.smoothness = 0.4 rotate.x.smoothness = 0.5 rotate.y.smoothness = 0.3 near = 2.0 ; Near plane distance far = 4096.0 ; Far plane distance fov = 45.0 ; Field of view (degrees), lower is narrow, higher is wide height.smoothness = 0.5 height.min = 16 Index: ps/trunk/source/ps/Profiler2.cpp =================================================================== --- ps/trunk/source/ps/Profiler2.cpp (revision 24339) +++ ps/trunk/source/ps/Profiler2.cpp (revision 24340) @@ -1,983 +1,997 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "lib/os_path.h" #include "ps/CLogger.h" +#include "ps/ConfigDB.h" #include "ps/CStr.h" #include "ps/Profiler2GPU.h" #include "ps/Pyrogenesis.h" #include "third_party/mongoose/mongoose.h" #include #include #include CProfiler2 g_Profiler2; const size_t CProfiler2::MAX_ATTRIBUTE_LENGTH = 256; // TODO: what's a good size? const size_t CProfiler2::BUFFER_SIZE = 4 * 1024 * 1024; const size_t CProfiler2::HOLD_BUFFER_SIZE = 128 * 1024; // 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}; thread_local CProfiler2::ThreadStorage* CProfiler2::m_CurrentStorage = nullptr; 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 == "/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); 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 + CStr listeningPort = "8000"; + CStr listeningServer = "127.0.0.1"; + CStr numThreads = "6"; + if (CConfigDB::IsInitialised()) + { + CFG_GET_VAL("profiler2.server.port", listeningPort); + CFG_GET_VAL("profiler2.server", listeningServer); + CFG_GET_VAL("profiler2.server.threads", numThreads); + } + + std::string listening_ports = fmt::format("{0}:{1}", listeningServer, listeningPort); + const char* options[] = { + "listening_ports", listening_ports.c_str(), + "num_threads", numThreads.c_str(), + nullptr }; 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()); 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); } void CProfiler2::RegisterCurrentThread(const std::string& name) { ENSURE(m_Initialised); // Must not register a thread more than once. ENSURE(m_CurrentStorage == nullptr); m_CurrentStorage = new ThreadStorage(*this, name); AddThreadStorage(m_CurrentStorage); RecordSyncMarker(); RecordEvent("thread start"); } void CProfiler2::AddThreadStorage(ThreadStorage* storage) { std::lock_guard lock(m_Mutex); m_Threads.push_back(std::unique_ptr(storage)); } void CProfiler2::RemoveThreadStorage(ThreadStorage* storage) { std::lock_guard lock(m_Mutex); m_Threads.erase(std::find_if(m_Threads.begin(), m_Threads.end(), [storage](const std::unique_ptr& s) { return s.get() == 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_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; using infoPerType = std::tuple >; using timeByTypeMap = std::unordered_map; timeByTypeMap 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); timeByTypeMap::iterator 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, std::min(size_t(len), CProfiler2::MAX_ATTRIBUTE_LENGTH)); CStr mess = CStr((const char*)message, len); if (!last_names.empty()) { timeByTypeMap::iterator 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 = { (float)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 (const std::pair& type : timeByType) { CProfiler2::SItem_dt_id item = { (float)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 std::string& attrib : std::get<2>(type.second)) { buffer[writePos] = (u8)CProfiler2::ITEM_ATTRIBUTE; writePos++; std::string basic = attrib; std::map::iterator 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 = { (float)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"); std::lock_guard lock(m_Mutex); stream << "{\"threads\":["; bool first_time = true; for (std::unique_ptr& storage : m_Threads) { if (!first_time) stream << ","; stream << "{\"name\":\"" << CStr(storage->GetName()).EscapeToPrintableASCII() << "\"}"; first_time = false; } 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) { 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) { visitor.OnEnter(lastTime + (double)item.dt, item.id); } break; } case CProfiler2::ITEM_LEAVE: { float leave_time; memcpy(&leave_time, buffer.c_str() + pos, sizeof(float)); pos += sizeof(float); if (lastTime >= 0) { 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) { 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"); std::lock_guard lock(m_Mutex); // lock against changes to m_Threads or deletions of ThreadStorage std::vector>::iterator it = std::find_if(m_Threads.begin(), m_Threads.end(), [&thread](std::unique_ptr& storage) { return storage->GetName() == thread; }); if (it == m_Threads.end()) return "cannot find named thread"; stream << "{\"events\":[\n"; stream << "[\n"; buffer = (*it)->GetBuffer(); } BufferVisitor_Dump visitor(stream); RunBufferVisitor(buffer, visitor); stream << "null]\n]}"; return NULL; } void CProfiler2::SaveToFile() { OsPath path = psLogDir()/"profile2.jsonp"; + debug_printf("Writing profile data to %s \n", path.string8().c_str()); + LOGMESSAGERENDER("Writing profile data to %s \n", path.string8().c_str()); std::ofstream stream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); ENSURE(stream.good()); stream << "profileDataCB({\"threads\": [\n"; bool first_time = true; for (std::unique_ptr& storage : m_Threads) { if (!first_time) stream << ",\n"; stream << "{\"name\":\"" << CStr(storage->GetName()).EscapeToPrintableASCII() << "\",\n"; stream << "\"data\": "; ConstructJSONResponse(stream, storage->GetName()); stream << "\n}"; first_time = false; } 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/tools/profiler2/Profiler2Report.js =================================================================== --- ps/trunk/source/tools/profiler2/Profiler2Report.js (revision 24339) +++ ps/trunk/source/tools/profiler2/Profiler2Report.js (revision 24340) @@ -1,426 +1,426 @@ // 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', + url: `http://127.0.0.1:${$("#gameport").val()}/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', + url: `http://127.0.0.1:${$("#gameport").val()}/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 (let thread = 0; thread < g_raw_data.threads.length; thread++) { let processed_data = process_raw_data(g_raw_data.threads[thread].data.events, range ); if (!processed_data.intervals.length && !processed_data.events.length) continue; g_data.threads[thread] = processed_data; g_data.threads[thread].intervals_by_type_frame = {}; if (!g_data.threads[thread].frames.length) continue // compute intervals by types and frames if there are frames. 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 (current_frame < g_data.threads[thread].frames.length && event.t0 > g_data.threads[thread].frames[current_frame].t1) { g_data.threads[thread].intervals_by_type_frame[type].push([]); current_frame++; } if (current_frame < g_data.threads[thread].frames.length) 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) { if (!data.length) return { 'frames': [], 'events': [], 'intervals': [], 'intervals_by_type' : {}, 'tmin': 0, 'tmax': 0 }; 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" : data[0][1], "tmax" : data[data.length-1][1] }; } 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; 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/profiler2.html =================================================================== --- ps/trunk/source/tools/profiler2/profiler2.html (revision 24339) +++ ps/trunk/source/tools/profiler2/profiler2.html (revision 24340) @@ -1,91 +1,95 @@ + + 0 A.D. profiler UI + +

Open reports

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

Click on the following timelines to zoom.

Analysis

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

Frequency Graph

Frame-by-Frame Graph

Report Comparison


 
-
\ No newline at end of file
+
Index: ps/trunk/source/tools/profiler2/profiler2.js
===================================================================
--- ps/trunk/source/tools/profiler2/profiler2.js	(revision 24339)
+++ ps/trunk/source/tools/profiler2/profiler2.js	(revision 24340)
@@ -1,503 +1,508 @@
 // 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',
+        url: `http://127.0.0.1:${$("#gameport").val()}/download`,
         success: function () {
         },
         error: function (jqXHR, textStatus, errorThrown) {
         }
     });
 }
 
 function get_history_data(report, thread, type)
 {
     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;
 
     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++)
     {
         ret.time_by_frame.push(0);
         for (let p = 0; p < data[i].length; p++)
             ret.time_by_frame[ret.time_by_frame.length-1] += interval_data[data[i][p]].duration;
     }
 
     // somehow JS sorts 0.03 lower than 3e-7 otherwise
     let sorted = ret.time_by_frame.slice(0).sort((a,b) => a-b);
     ret.max = sorted[sorted.length-1];
     avg = sorted[Math.round(avg.length/2)];
 
     if (ret.max > avg * 3)
         ret.log_scale = true;
 
     return ret;
 }
 
 function draw_frequency_graph()
 {
     let canvas = document.getElementById("canvas_frequency");
     canvas._tooltips = [];
 
     let context = canvas.getContext("2d");
     context.clearRect(0, 0, canvas.width, canvas.height);
 
     let legend = document.getElementById("frequency_graph").querySelector("aside");
     legend.innerHTML = "";
 
     if (!g_active_elements.length)
         return;
 
     var series_data = {};
     var use_log_scale = null;
 
     var x_scale = 0;
     var y_scale = 0;
     var padding = 10;
 
     var item_nb = 0;
 
     var tooltip_helper = {};
 
     for (let typeI in g_active_elements)
     {
         for (let rep in g_reports)
         {
             item_nb++;
             let data = get_history_data(rep, g_main_thread, g_active_elements[typeI]);
             let name = rep + "/" + g_active_elements[typeI];
             if (document.getElementById('fulln').checked)
                 series_data[name] = data.time_by_frame.sort((a,b) => a-b);
             else
                 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;
 
         let nb = document.createElement("p");
         nb.style.borderColor = colour;
         nb.textContent = type + " - n=" + time_by_frame.length;
         legend.appendChild(nb);
 
         for (var i = 0; i < time_by_frame.length; i++)
         {
             let x0 = i/time_by_frame.length*(canvas.width-padding*2) + padding;
             if (i == 0)
                 x0 = 0;
             let x1 = (i+1)/time_by_frame.length*(canvas.width-padding*2) + padding;
             if (i == time_by_frame.length-1)
                 x1 = (time_by_frame.length-1)*canvas.width;
 
             let y = time_by_frame[i]/y_scale;
             if (use_log_scale)
                 y = Math.log10(1 + time_by_frame[i]/y_scale * 9);
 
             context.globalCompositeOperation = "lighter";
 
             context.beginPath();
             context.strokeStyle = colour
             context.lineWidth = 0.5;
             context.moveTo(x0,canvas.height * (1 - last_val));
             context.lineTo(x1,canvas.height * (1 - y));
             context.stroke();
 
             last_val = y;
             if (!tooltip_helper[Math.floor(x0)])
                 tooltip_helper[Math.floor(x0)] = [];
             tooltip_helper[Math.floor(x0)].push([y, type]);
         }
         id++;
     }
 
     for (let i in tooltip_helper)
     {
         let tooltips = tooltip_helper[i];
         let text = "";
         for (let j in tooltips)
             if (tooltips[j][0] != undefined && text.search(tooltips[j][1])===-1)
                 text += "Series " + tooltips[j][1] + ": " + time_label((tooltips[j][0])*y_scale,1) + "
"; canvas._tooltips.push({ 'x0': +i, 'x1': +i+1, 'y0': 0, 'y1': canvas.height, 'text': function(text) { return function() { return text; } }(text) }); } set_tooltip_handlers(canvas); [0.02,0.05,0.1,0.25,0.5,0.75].forEach(function(y_val) { let y = y_val; if (use_log_scale) y = Math.log10(1 + y_val * 9); context.beginPath(); context.lineWidth="1"; context.strokeStyle = "rgba(0,0,0,0.2)"; context.moveTo(0,canvas.height * (1- y)); context.lineTo(canvas.width,canvas.height * (1 - y)); context.stroke(); context.fillStyle = "gray"; context.font = "10px Arial"; context.textAlign="left"; context.fillText(time_label(y*y_scale,0), 2, canvas.height * (1 - y) - 2 ); }); } function draw_history_graph() { let canvas = document.getElementById("canvas_history"); canvas._tooltips = []; let context = canvas.getContext("2d"); context.clearRect(0, 0, canvas.width, canvas.height); let legend = document.getElementById("history_graph").querySelector("aside"); legend.innerHTML = ""; if (!g_active_elements.length) return; var series_data = {}; var use_log_scale = null; var frames_nb = Infinity; var x_scale = 0; var y_scale = 0; var item_nb = 0; var tooltip_helper = {}; for (let typeI in g_active_elements) { 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; item_nb++; let data = get_history_data(rep, g_main_thread, g_active_elements[typeI]); if (!document.getElementById('smooth').value) series_data[rep + "/" + g_active_elements[typeI]] = data.time_by_frame; else series_data[rep + "/" + g_active_elements[typeI]] = smooth_1D_array(data.time_by_frame, +document.getElementById('smooth').value); if (data.max > y_scale) y_scale = data.max; if (use_log_scale === null && data.log_scale) use_log_scale = true; } } if (use_log_scale) { let legend_item = document.createElement("p"); legend_item.style.borderColor = "transparent"; legend_item.textContent = " -- log y scale -- "; legend.appendChild(legend_item); } canvas.width = Math.max(frames_nb,600); x_scale = frames_nb / canvas.width; let id = 0; for (let type in series_data) { let colour = graph_colour(id); let legend_item = document.createElement("p"); legend_item.style.borderColor = colour; legend_item.textContent = type; legend.appendChild(legend_item); let time_by_frame = series_data[type]; let last_val = 0; for (var i = 0; i < frames_nb; i++) { let smoothed_time = time_by_frame[i];//smooth_1D(time_by_frame.slice(0), i, 3); let y = smoothed_time/y_scale; if (use_log_scale) y = Math.log10(1 + smoothed_time/y_scale * 9); if (item_nb === 1) { context.beginPath(); context.fillStyle = colour; context.fillRect(i/x_scale,canvas.height,1/x_scale,-y*canvas.height); } else { 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]); } id++; } for (let i in tooltip_helper) { let tooltips = tooltip_helper[i]; let text = "Frame " + i*x_scale + "
"; for (let j in tooltips) if (tooltips[j][0] != undefined && text.search(tooltips[j][1])===-1) text += "Series " + tooltips[j][1] + ": " + time_label((tooltips[j][0])*y_scale,1) + "
"; canvas._tooltips.push({ 'x0': +i, 'x1': +i+1, 'y0': 0, 'y1': canvas.height, 'text': function(text) { return function() { return text; } }(text) }); } set_tooltip_handlers(canvas); [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 ); }); } function compare_reports() { let section = document.getElementById("comparison"); section.innerHTML = "

Report Comparison

"; if (g_reports.length < 2) { section.innerHTML += "

Too few reports loaded

"; return; } if (g_active_elements.length != 1) { section.innerHTML += "

Too many of too few elements selected

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

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

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

Choices

"; var types = {}; var data = report.data().threads[thread]; for (let i = 0; i < data.intervals.length; i++) types[data.intervals[i].id] = 0; var sorted_keys = Object.keys(types).sort(); for (let key in sorted_keys) { let type = sorted_keys[key]; let p = document.createElement("p"); p.textContent = type; if (g_active_elements.indexOf(p.textContent) !== -1) p.className = "active"; choices.appendChild(p); p.onclick = function() { if (g_active_elements.indexOf(p.textContent) !== -1) { p.className = ""; g_active_elements = g_active_elements.filter( x => x != p.textContent); update_analysis(); return; } g_active_elements.push(p.textContent); p.className = "active"; update_analysis(); } } update_analysis(); } function update_analysis() { compare_reports(); draw_history_graph(); draw_frequency_graph(); } function load_report_from_file(evt) { var file = evt.target.files[0]; if (!file) return; load_report(false, file); evt.target.value = null; } function load_report(trylive, file) { if (g_loading_timeout != undefined) return; 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); g_reports.push(Profiler2Report(on_report_loaded, trylive, file)); g_loading_timeout = setTimeout(function() { on_report_loaded(false); }, 5000); } function on_report_loaded(success) { let element = document.getElementById("Report" + (g_reports.length-1)); let report = g_reports[g_reports.length-1]; if (!success) { element.className = "fail"; setTimeout(function() { element.parentNode.removeChild(element); clearTimeout(g_loading_timeout); g_loading_timeout = null; }, 1000 ); g_reports = g_reports.slice(0,-1); if (g_reports.length === 0) g_current_report = null; return; } clearTimeout(g_loading_timeout); g_loading_timeout = null; select_report(+element.dataset.id); element.onclick = function() { select_report(+element.dataset.id);}; } function select_report(id) { if (g_current_report != undefined) document.getElementById("Report" + g_current_report).className = ""; document.getElementById("Report" + id).className = "active"; g_current_report = id; // Load up our canvas g_report_draw.update_display(g_reports[id],{"seconds":5}); recompute_choices(g_reports[id], g_main_thread); } window.onload = function() { // Try loading the report live load_report(true, {"name":"live"}); // add new reports document.getElementById('report_load_input').addEventListener('change', load_report_from_file, false); } + + +function updatePort() { + document.location.reload(); +}