Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Snapshots #459

Merged
merged 14 commits into from Aug 31, 2020
11 changes: 4 additions & 7 deletions Rakefile
Expand Up @@ -16,8 +16,10 @@ require 'rspec/core'
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
pattern = ARGV[1] || 'spec/**/*_spec.rb'
spec.pattern = FileList[pattern]
excluded = 'spec/support/*.rb'
spec.pattern = FileList[pattern] - FileList[excluded]
spec.verbose = false
# spec.rspec_opts = ["-p"] # turns on profiling
end

desc "builds a gem"
Expand All @@ -26,7 +28,7 @@ task build: :update_asset_version do
end

desc "compile sass"
task compile_sass: :copy_files do
task :compile_sass do
require "sassc"
scss = File.read("lib/html/includes.scss")
css = SassC::Engine.new(scss).render
Expand Down Expand Up @@ -120,8 +122,3 @@ task :client_dev do
rescue Interrupt
listener.stop
end

desc "copy files from other parts of the tree"
task :copy_files do
# TODO grab files from MiniProfiler/UI
end
34 changes: 34 additions & 0 deletions lib/html/includes.css
@@ -1,9 +1,20 @@
@charset "UTF-8";
.mp-snapshots,
.profiler-result,
.profiler-queries {
color: #555;
line-height: 1;
font-size: 12px; }
.mp-snapshots pre,
.mp-snapshots code,
.mp-snapshots label,
.mp-snapshots table,
.mp-snapshots tbody,
.mp-snapshots thead,
.mp-snapshots tfoot,
.mp-snapshots tr,
.mp-snapshots th,
.mp-snapshots td,
.profiler-result pre,
.profiler-result code,
.profiler-result label,
Expand Down Expand Up @@ -33,22 +44,33 @@
background-color: transparent;
overflow: visible;
max-height: none; }
.mp-snapshots table,
.profiler-result table,
.profiler-queries table {
border-collapse: collapse;
border-spacing: 0; }
.mp-snapshots a,
.mp-snapshots a:hover,
.profiler-result a,
.profiler-result a:hover,
.profiler-queries a,
.profiler-queries a:hover {
cursor: pointer;
color: #0077cc; }
.mp-snapshots a,
.profiler-result a,
.profiler-queries a {
text-decoration: none; }
.mp-snapshots a:hover,
.profiler-result a:hover,
.profiler-queries a:hover {
text-decoration: underline; }
.mp-snapshots .custom-fields-title,
.profiler-result .custom-fields-title,
.profiler-queries .custom-fields-title {
color: #555;
font: Helvetica, Arial, sans-serif;
font-size: 14px; }

.profiler-result {
font-family: Helvetica, Arial, sans-serif; }
Expand Down Expand Up @@ -407,3 +429,15 @@
background: #ffffbb; }
100% {
background: #fff; } }

.mp-snapshots {
font-family: Helvetica, Arial, sans-serif;
font-size: 16px; }
.mp-snapshots .snapshots-table thead {
background: #6a737c;
color: #ffffff; }
.mp-snapshots .snapshots-table th, .mp-snapshots .snapshots-table td {
padding: 5px;
box-sizing: border-box; }
.mp-snapshots .snapshots-table th {
border-right: 1px solid #ffffff; }
44 changes: 44 additions & 0 deletions lib/html/includes.js
Expand Up @@ -673,6 +673,19 @@ var MiniProfiler = (function() {
});
};

var initSnapshots = function initSnapshots(dataElement) {
var data = JSON.parse(dataElement.textContent);
var temp = document.createElement("DIV");
if (data.page === "overview") {
temp.innerHTML = MiniProfiler.templates.snapshotsGroupsList(data);
} else if (data.group_name) {
temp.innerHTML = MiniProfiler.templates.snapshotsList(data);
}
Array.from(temp.children).forEach(function (child) {
document.body.appendChild(child);
});
};

var initControls = function initControls(container) {
if (options.showControls) {
var _controls = document.createElement("div");
Expand Down Expand Up @@ -1003,6 +1016,12 @@ var MiniProfiler = (function() {
})();

var doInit = function doInit() {
var snapshotsElement = document.getElementById("snapshots-data");
if (snapshotsElement != null) {
initSnapshots(snapshotsElement);
return;
}

// when rendering a shared, full page, this div will exist
container = document.querySelectorAll(".profiler-result-full");

Expand Down Expand Up @@ -1396,6 +1415,31 @@ var MiniProfiler = (function() {
},
showTotalSqlCount: function showTotalSqlCount() {
return options.showTotalSqlCount;
},
timestampToRelative: function timestampToRelative(timestamp) {
var now = Math.round((new Date()).getTime() / 1000);
timestamp = Math.round(timestamp / 1000);
var diff = now - timestamp;
if (diff < 60) {
return "< 1 minute";
}
var buildDisplayTime = function buildDisplayTime(num, unit) {
var res = num + " " + unit;
if (num !== 1) {
res += "s";
}
return res;
}
diff = Math.round(diff / 60);
if (diff <= 60) {
return buildDisplayTime(diff, "minute");
}
diff = Math.round(diff / 60);
if (diff <= 24) {
return buildDisplayTime(diff, "hour");
}
diff = Math.round(diff / 24);
return buildDisplayTime(diff, "day");
}
};
})();
Expand Down
33 changes: 29 additions & 4 deletions lib/html/includes.scss
Expand Up @@ -14,9 +14,10 @@ $codeFonts: Consolas, monospace, serif;
$zindex: 2147483640; // near 32bit max 2147483647

// do some resets
.mp-snapshots,
.profiler-result,
.profiler-queries {
color: #555;
color: $textColor;
line-height: 1;
font-size: 12px;

Expand Down Expand Up @@ -55,6 +56,11 @@ $zindex: 2147483640; // near 32bit max 2147483647
text-decoration: underline;
}
}
.custom-fields-title {
color: $textColor;
font: $normalFonts;
font-size: 14px;
}
}

// styles shared between popup view and full view
Expand Down Expand Up @@ -199,7 +205,7 @@ $zindex: 2147483640; // near 32bit max 2147483647

th {
background-color: #fff;
border-bottom: 1px solid #555;
border-bottom: 1px solid $textColor;
font-weight: bold;
padding: 15px;
white-space: nowrap;
Expand Down Expand Up @@ -452,7 +458,7 @@ $zindex: 2147483640; // near 32bit max 2147483647
line-height: 18px;
overflow: auto;

@include box-shadow(0px, 1px, 15px, #555);
@include box-shadow(0px, 1px, 15px, $textColor);

.profiler-info {
margin-bottom: 3px;
Expand Down Expand Up @@ -592,7 +598,7 @@ $zindex: 2147483640; // near 32bit max 2147483647
}
th {
font-size: 16px;
color: #555;
color: $textColor;
line-height: 20px;
}

Expand All @@ -617,3 +623,22 @@ $zindex: 2147483640; // near 32bit max 2147483647
background: #fff;
}
}

.mp-snapshots {
font-family: $normalFonts;
font-size: 16px;

.snapshots-table {
thead {
background: #6a737c;
color: #ffffff;
}
th, td {
padding: 5px;
box-sizing: border-box;
}
th {
border-right: 1px solid #ffffff;
}
}
}
68 changes: 68 additions & 0 deletions lib/html/includes.tmpl
Expand Up @@ -92,6 +92,19 @@
</tfoot>
</table>
{{?}}
{{? it.custom_fields && Object.keys(it.custom_fields).length > 0 }}
<p class="custom-fields-title">Snapshot custom fields</p>
<table class="profiler-timings">
<tbody>
{{~ Object.keys(it.custom_fields) :key }}
<tr>
<td class="profiler-label">{{= key }}</td>
<td class="profiler-label">{{= it.custom_fields[key] }}</td>
</tr>
{{~}}
</tbody>
</table>
{{?}}
</div>
</div>

Expand Down Expand Up @@ -216,3 +229,58 @@
</td>
</tr>
</script>

<script id="snapshotsGroupsList" type="text/x-dot-tmpl">
{{? it.list && it.list.length }}
<table class="snapshots-table">
<thead>
<tr>
<th>Requests Group</th>
<th>Worst Time (ms)</th>
</tr>
</thead>
<tbody>
{{~ it.list :row}}
<tr>
<td><a href="{{= row.url }}">{{= row.name }}</a></td>
<td>{{= MiniProfiler.formatDuration(row.worst_score) }}</td>
</tr>
{{~}}
</tbody>
</table>
{{??}}
<h2>No snapshots exist</h2>
{{?}}
</script>

<script id="snapshotsList" type="text/x-dot-tmpl">
{{? it.list && it.list.length }}
<h2>Snapshots for {{= it.group_name }}</h2>
<table class="snapshots-table">
<thead>
<tr>
<th>ID</th>
<th>Duration (ms)</th>
<th>Age</th>
</tr>
</thead>
<tbody>
{{~ it.list :row}}
<tr>
<td><a href="{{= row.url }}">
{{= row.id }}
</a></td>
<td>{{= MiniProfiler.formatDuration(row.duration) }}</td>
<td>
{{? row.timestamp }}
{{= MiniProfiler.timestampToRelative(row.timestamp) }}
{{?}}
</td>
</tr>
{{~}}
</tbody>
</table>
{{??}}
<h2>No snapshots for {{= it.group_name }}</h2>
{{?}}
</script>
10 changes: 9 additions & 1 deletion lib/html/vendor.js
Expand Up @@ -7,7 +7,7 @@
MiniProfiler.templates = {};
MiniProfiler.templates["profilerTemplate"] = function anonymous(it
) {
var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_duplicate_sql_timings){out+='profiler-warning';}out+='"> ';if(it.has_duplicate_sql_timings){out+='<span class="profiler-nuclear">!</span>';}out+=' <span class="profiler-number"> '+( MiniProfiler.formatDuration(it.duration_milliseconds))+' <span class="profiler-unit">ms</span> </span> ';if(MiniProfiler.showTotalSqlCount()){out+=' <span class="profiler-number"> '+( it.sql_count)+' <span class="profiler-unit">sql</span> </span> ';}out+=' </div> <div class="profiler-popup"> <div class="profiler-info"> <span class="profiler-name"> '+( it.name)+' <span class="profiler-overall-duration">('+( MiniProfiler.formatDuration(it.duration_milliseconds))+' ms)</span> </span> <span class="profiler-server-time">'+( it.machine_name)+' on '+( MiniProfiler.renderDate(it.started))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th></th> <th>duration (ms)</th> <th class="profiler-duration-with-children">with children (ms)</th> <th class="time-from-start">from start (ms)</th> ';if(it.has_sql_timings){out+=' <th colspan="2">query time (ms)</th> ';}out+=' ';var arr1=it.custom_timing_names;if(arr1){var value,i1=-1,l1=arr1.length-1;while(i1<l1){value=arr1[i1+=1];out+=' <th colspan="2">'+( value.toLowerCase() )+' (ms)</th> ';} } out+=' </tr> </thead> <tbody> '+( MiniProfiler.templates.timingTemplate({timing: it.root, page: it}) )+' </tbody> <tfoot> <tr> <td colspan="3"> ';if(!it.client_timings){out+=' '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' ';}out+=' <a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a> </td> ';if(it.has_sql_timings){out+=' <td colspan="2" class="profiler-number profiler-percent-in-sql" title="'+( MiniProfiler.getSqlTimingsCount(it.root) )+' queries spent '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in sql</span> </td> ';}out+=' ';var arr2=it.custom_timing_names;if(arr2){var value,i2=-1,l2=arr2.length-1;while(i2<l2){value=arr2[i2+=1];out+=' <td colspan="2" class="profiler-number profiler-percentage-in-sql" title="'+( it.custom_timing_stats[value].count )+' '+( value.toLowerCase() )+' invocations spent '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in '+( value.toLowerCase() )+'</span> </td> ';} } out+=' </tr> </tfoot> </table> ';if(it.client_timings){out+=' <table class="profiler-timings profiler-client-timings"> <thead> <tr> <th>client event</th> <th>duration (ms)</th> <th>from start (ms)</th> </tr> </thead> <tbody> ';var arr3=MiniProfiler.getClientTimings(it.client_timings);if(arr3){var value,i3=-1,l3=arr3.length-1;while(i3<l3){value=arr3[i3+=1];out+=' <tr class="';if(value.isTrivial){out+='profiler-trivial';}out+='"> <td class="profiler-label">'+( value.name )+'</td> <td class="profiler-duration"> ';if(value.duration >= 0){out+=' <span class="profiler-unit"></span>'+( MiniProfiler.formatDuration(value.duration) )+' ';}out+=' </td> <td class="profiler-duration time-from-start"> <span class="profiler-unit">+</span>'+( MiniProfiler.formatDuration(value.start) )+' </td> </tr> ';} } out+=' </tbody> <tfoot> <td colspan="3"> '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' </td> </tfoot> </table> ';}out+=' </div> </div> ';if(it.has_sql_timings){out+=' <div class="profiler-queries"> <table> <thead> <tr> <th style="text-align:right">step<br />time from start<br />query type<br />duration</th> <th style="text-align:left">call stack<br />query</th> </tr> </thead> <tbody> ';var arr4=MiniProfiler.getSqlTimings(it.root);if(arr4){var value,index=-1,l4=arr4.length-1;while(index<l4){value=arr4[index+=1];out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.prevGap}) )+' '+( MiniProfiler.templates.sqlTimingTemplate({i: index, s: value}) )+' ';if(value.nextGap){out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.nextGap}) )+' ';}out+=' ';} } out+=' </tbody> </table> <p class="profiler-trivial-gap-container"> <a class="profiler-toggle-trivial-gaps">show trivial gaps</a> </p> </div> ';}out+=' </div>';return out;
var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_duplicate_sql_timings){out+='profiler-warning';}out+='"> ';if(it.has_duplicate_sql_timings){out+='<span class="profiler-nuclear">!</span>';}out+=' <span class="profiler-number"> '+( MiniProfiler.formatDuration(it.duration_milliseconds))+' <span class="profiler-unit">ms</span> </span> ';if(MiniProfiler.showTotalSqlCount()){out+=' <span class="profiler-number"> '+( it.sql_count)+' <span class="profiler-unit">sql</span> </span> ';}out+=' </div> <div class="profiler-popup"> <div class="profiler-info"> <span class="profiler-name"> '+( it.name)+' <span class="profiler-overall-duration">('+( MiniProfiler.formatDuration(it.duration_milliseconds))+' ms)</span> </span> <span class="profiler-server-time">'+( it.machine_name)+' on '+( MiniProfiler.renderDate(it.started))+'</span> </div> <div class="profiler-output"> <table class="profiler-timings"> <thead> <tr> <th></th> <th>duration (ms)</th> <th class="profiler-duration-with-children">with children (ms)</th> <th class="time-from-start">from start (ms)</th> ';if(it.has_sql_timings){out+=' <th colspan="2">query time (ms)</th> ';}out+=' ';var arr1=it.custom_timing_names;if(arr1){var value,i1=-1,l1=arr1.length-1;while(i1<l1){value=arr1[i1+=1];out+=' <th colspan="2">'+( value.toLowerCase() )+' (ms)</th> ';} } out+=' </tr> </thead> <tbody> '+( MiniProfiler.templates.timingTemplate({timing: it.root, page: it}) )+' </tbody> <tfoot> <tr> <td colspan="3"> ';if(!it.client_timings){out+=' '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' ';}out+=' <a class="profiler-toggle-duration-with-children" title="toggles column with aggregate child durations">show time with children</a> </td> ';if(it.has_sql_timings){out+=' <td colspan="2" class="profiler-number profiler-percent-in-sql" title="'+( MiniProfiler.getSqlTimingsCount(it.root) )+' queries spent '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.duration_milliseconds_in_sql / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in sql</span> </td> ';}out+=' ';var arr2=it.custom_timing_names;if(arr2){var value,i2=-1,l2=arr2.length-1;while(i2<l2){value=arr2[i2+=1];out+=' <td colspan="2" class="profiler-number profiler-percentage-in-sql" title="'+( it.custom_timing_stats[value].count )+' '+( value.toLowerCase() )+' invocations spent '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration) )+' ms of total request time"> '+( MiniProfiler.formatDuration(it.custom_timing_stats[value].duration / it.duration_milliseconds * 100) )+' <span class="profiler-unit">% in '+( value.toLowerCase() )+'</span> </td> ';} } out+=' </tr> </tfoot> </table> ';if(it.client_timings){out+=' <table class="profiler-timings profiler-client-timings"> <thead> <tr> <th>client event</th> <th>duration (ms)</th> <th>from start (ms)</th> </tr> </thead> <tbody> ';var arr3=MiniProfiler.getClientTimings(it.client_timings);if(arr3){var value,i3=-1,l3=arr3.length-1;while(i3<l3){value=arr3[i3+=1];out+=' <tr class="';if(value.isTrivial){out+='profiler-trivial';}out+='"> <td class="profiler-label">'+( value.name )+'</td> <td class="profiler-duration"> ';if(value.duration >= 0){out+=' <span class="profiler-unit"></span>'+( MiniProfiler.formatDuration(value.duration) )+' ';}out+=' </td> <td class="profiler-duration time-from-start"> <span class="profiler-unit">+</span>'+( MiniProfiler.formatDuration(value.start) )+' </td> </tr> ';} } out+=' </tbody> <tfoot> <td colspan="3"> '+( MiniProfiler.templates.linksTemplate({timing: it.root, page: it}) )+' </td> </tfoot> </table> ';}out+=' ';if(it.custom_fields && Object.keys(it.custom_fields).length > 0){out+=' <p class="custom-fields-title">Snapshot custom fields</p> <table class="profiler-timings"> <tbody> ';var arr4=Object.keys(it.custom_fields);if(arr4){var key,i4=-1,l4=arr4.length-1;while(i4<l4){key=arr4[i4+=1];out+=' <tr> <td class="profiler-label">'+( key )+'</td> <td class="profiler-label">'+( it.custom_fields[key] )+'</td> </tr> ';} } out+=' </tbody> </table> ';}out+=' </div> </div> ';if(it.has_sql_timings){out+=' <div class="profiler-queries"> <table> <thead> <tr> <th style="text-align:right">step<br />time from start<br />query type<br />duration</th> <th style="text-align:left">call stack<br />query</th> </tr> </thead> <tbody> ';var arr5=MiniProfiler.getSqlTimings(it.root);if(arr5){var value,index=-1,l5=arr5.length-1;while(index<l5){value=arr5[index+=1];out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.prevGap}) )+' '+( MiniProfiler.templates.sqlTimingTemplate({i: index, s: value}) )+' ';if(value.nextGap){out+=' '+( MiniProfiler.templates.sqlGapTemplate({g: value.nextGap}) )+' ';}out+=' ';} } out+=' </tbody> </table> <p class="profiler-trivial-gap-container"> <a class="profiler-toggle-trivial-gaps">show trivial gaps</a> </p> </div> ';}out+=' </div>';return out;
}
MiniProfiler.templates["linksTemplate"] = function anonymous(it
) {
Expand All @@ -25,6 +25,14 @@ MiniProfiler.templates["sqlGapTemplate"] = function anonymous(it
) {
var out=' <tr class="profiler-gap-info';if(it.g.duration < 4){out+=' profiler-trivial-gaps';}out+='"> <td class="profiler-info"> '+( it.g.duration )+' <span class="profiler-unit">ms</span> </td> <td class="query"> <div>'+( it.g.topReason.name )+' &mdash; '+( it.g.topReason.duration.toFixed(2) )+' <span class="profiler-unit">ms</span></div> </td> </tr>';return out;
}
MiniProfiler.templates["snapshotsGroupsList"] = function anonymous(it
) {
var out=' ';if(it.list && it.list.length){out+=' <table class="snapshots-table"> <thead> <tr> <th>Requests Group</th> <th>Worst Time (ms)</th> </tr> </thead> <tbody> ';var arr1=it.list;if(arr1){var row,i1=-1,l1=arr1.length-1;while(i1<l1){row=arr1[i1+=1];out+=' <tr> <td><a href="'+( row.url )+'">'+( row.name )+'</a></td> <td>'+( MiniProfiler.formatDuration(row.worst_score) )+'</td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots exist</h2> ';}return out;
}
MiniProfiler.templates["snapshotsList"] = function anonymous(it
) {
var out=' ';if(it.list && it.list.length){out+=' <h2>Snapshots for '+( it.group_name )+'</h2> <table class="snapshots-table"> <thead> <tr> <th>ID</th> <th>Duration (ms)</th> <th>Age</th> </tr> </thead> <tbody> ';var arr1=it.list;if(arr1){var row,i1=-1,l1=arr1.length-1;while(i1<l1){row=arr1[i1+=1];out+=' <tr> <td><a href="'+( row.url )+'"> '+( row.id )+' </a></td> <td>'+( MiniProfiler.formatDuration(row.duration) )+'</td> <td> ';if(row.timestamp){out+=' '+( MiniProfiler.timestampToRelative(row.timestamp) )+' ';}out+=' </td> </tr> ';} } out+=' </tbody> </table> ';}else{out+=' <h2>No snapshots for '+( it.group_name )+'</h2> ';}return out;
}

if (typeof prettyPrint === "undefined") {
// prettify.js
Expand Down
2 changes: 1 addition & 1 deletion lib/mini_profiler/asset_version.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
module Rack
class MiniProfiler
ASSET_VERSION = '435a6923efca239f3ae0a09ac2d09acb'
ASSET_VERSION = '1cb03369a105db3d0d6f4a7c2b3433b0'
end
end