rack-mini-profiler 0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack-mini-profiler might be problematic. Click here for more details.

@@ -0,0 +1,82 @@
1
+ # rack-mini-profiler
2
+
3
+ Middleware that displays speed badge for every html page.
4
+
5
+ ## What does it do
6
+
7
+ MiniProfiler keeps you aware of your site's performance as you are developing it.
8
+ It does this by....
9
+
10
+ `env['profiler.mini']` is the profiler
11
+
12
+ ## Using mini-profiler in your app
13
+
14
+ Install/add to Gemfile
15
+
16
+ ```ruby
17
+ gem 'rack-mini-profiler'
18
+ ```
19
+
20
+ Add it to your middleware stack:
21
+
22
+ Using Rails:
23
+
24
+ All you have to do is include the Gem and you're good to go.
25
+
26
+ Using Builder:
27
+
28
+ ```ruby
29
+ require 'rack-mini-profiler'
30
+ builder = Rack::Builder.new do
31
+ use Rack::MiniProfiler
32
+
33
+ map('/') { run get }
34
+ end
35
+ ```
36
+
37
+ Using Sinatra:
38
+
39
+ ```ruby
40
+ require 'rack-mini-profiler'
41
+ class MyApp < Sinatra::Base
42
+ use Rack::MiniProfiler
43
+ end
44
+ ```
45
+
46
+ ## Running the Specs
47
+
48
+ ```
49
+ $ rake build
50
+ $ rake spec
51
+ ```
52
+
53
+ Additionally you can also run `autotest` if you like.
54
+
55
+ ## Configuration Options
56
+
57
+ You can set configuration options using the configuration accessor on Rack::MiniProfiler:
58
+
59
+ ```
60
+ # Have Mini Profiler show up on the right
61
+ Rack::MiniProfiler.configuration[:position] = 'right'
62
+ ```
63
+
64
+ In a Rails app, this can be done conveniently in an initializer such as config/initializers/mini_profiler.rb.
65
+
66
+ ## Available Options
67
+
68
+ * authorize_cb - A lambda callback you can set to determine whether or not mini_profiler should be visible on a given request. Default in a Rails environment is only on in development mode. If in a Rack app, the default is always on.
69
+ * position - Can either be 'right' or 'left'. Default is 'left'.
70
+ * skip_schema_queries - Whether or not you want to log the queries about the schema of your tables. Default is 'true'
71
+
72
+
73
+ ## TODO: prior to release - pull requests welcome
74
+
75
+ - Stack Traces for SQL called (added but mental, needs to be filtered to something usable)
76
+ - Decide if we hook up SQL at the driver level (eg mysql gem) or library level (eg active record) - my personal perference is to do driver level hooks (Sam)
77
+ - Add automatic instrumentation for Rails (Controller times, Action times, Partial times, Layout times)
78
+ - Grab / display the parameters of SQL executed for parameterized SQL
79
+ - Beef up the documentation
80
+ - Auto-wire-up rails middleware
81
+ - Review our API and ensure it is trivial
82
+
@@ -0,0 +1,419 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.Web;
5
+ using System.Web.Routing;
6
+ using System.Linq;
7
+
8
+ using StackExchange.Profiling.Helpers;
9
+ using System.Text;
10
+ using System.Collections.Concurrent;
11
+
12
+ namespace StackExchange.Profiling.UI
13
+ {
14
+ /// <summary>
15
+ /// Understands how to route and respond to MiniProfiler UI urls.
16
+ /// </summary>
17
+ public class MiniProfilerHandler : IRouteHandler, IHttpHandler
18
+ {
19
+ internal static HtmlString RenderIncludes(MiniProfiler profiler, RenderPosition? position = null, bool? showTrivial = null, bool? showTimeWithChildren = null, int? maxTracesToShow = null, bool? showControls = null, bool? useExistingjQuery = null)
20
+ {
21
+ string format = GetResource("include.partial.html");
22
+
23
+ var result = "";
24
+
25
+ if (profiler != null)
26
+ {
27
+ // HACK: unviewed ids are added to this list during Storage.Save, but we know we haven't see the current one yet,
28
+ // so go ahead and add it to the end - it's usually the only id, but if there was a redirect somewhere, it'll be there, too
29
+ MiniProfiler.Settings.EnsureStorageStrategy();
30
+
31
+ var authorized =
32
+ MiniProfiler.Settings.Results_Authorize == null ||
33
+ MiniProfiler.Settings.Results_Authorize(HttpContext.Current.Request);
34
+
35
+ List<Guid> ids;
36
+ if (authorized)
37
+ {
38
+ ids = MiniProfiler.Settings.Storage.GetUnviewedIds(profiler.User);
39
+ ids.Add(profiler.Id);
40
+ }
41
+ else
42
+ {
43
+ ids = new List<Guid> { profiler.Id };
44
+ }
45
+
46
+ result = format.Format(new
47
+ {
48
+ path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash(),
49
+ version = MiniProfiler.Settings.Version,
50
+ ids = ids.ToJson(),
51
+ position = (position ?? MiniProfiler.Settings.PopupRenderPosition).ToString().ToLower(),
52
+ showTrivial = showTrivial ?? MiniProfiler.Settings.PopupShowTrivial ? "true" : "false",
53
+ showChildren = showTimeWithChildren ?? MiniProfiler.Settings.PopupShowTimeWithChildren ? "true" : "false",
54
+ maxTracesToShow = maxTracesToShow ?? MiniProfiler.Settings.PopupMaxTracesToShow,
55
+ showControls = showControls ?? MiniProfiler.Settings.ShowControls ? "true" : "false",
56
+ currentId = profiler.Id,
57
+ authorized = authorized ? "true" : "false",
58
+ useExistingjQuery = useExistingjQuery ?? MiniProfiler.Settings.UseExistingjQuery ? "true" : "false"
59
+ });
60
+
61
+ }
62
+
63
+ return new HtmlString(result);
64
+ }
65
+
66
+ /// <summary>
67
+ /// Usually called internally, sometimes you may clear the routes during the apps lifecycle, if you do that call this to bring back mp
68
+ /// </summary>
69
+ public static void RegisterRoutes()
70
+ {
71
+
72
+ var routes = RouteTable.Routes;
73
+ var handler = new MiniProfilerHandler();
74
+ var prefix = MiniProfiler.Settings.RouteBasePath.Replace("~/", "").EnsureTrailingSlash();
75
+
76
+ using (routes.GetWriteLock())
77
+ {
78
+ var route = new Route(prefix + "{filename}", handler)
79
+ {
80
+ // we have to specify these, so no MVC route helpers will match, e.g. @Html.ActionLink("Home", "Index", "Home")
81
+ Defaults = new RouteValueDictionary( new { controller = "MiniProfilerHandler", action = "ProcessRequest" }),
82
+ Constraints = new RouteValueDictionary( new { controller = "MiniProfilerHandler", action = "ProcessRequest" })
83
+ };
84
+
85
+ // put our routes at the beginning, like a boss
86
+ routes.Insert(0, route);
87
+ }
88
+ }
89
+
90
+ /// <summary>
91
+ /// Returns this <see cref="MiniProfilerHandler"/> to handle <paramref name="requestContext"/>.
92
+ /// </summary>
93
+ public IHttpHandler GetHttpHandler(RequestContext requestContext)
94
+ {
95
+ return this; // elegant? I THINK SO.
96
+ }
97
+
98
+ /// <summary>
99
+ /// Try to keep everything static so we can easily be reused.
100
+ /// </summary>
101
+ public bool IsReusable
102
+ {
103
+ get { return true; }
104
+ }
105
+
106
+ /// <summary>
107
+ /// Returns either includes' css/javascript or results' html.
108
+ /// </summary>
109
+ public void ProcessRequest(HttpContext context)
110
+ {
111
+ string output;
112
+ string path = context.Request.AppRelativeCurrentExecutionFilePath;
113
+
114
+ switch (Path.GetFileNameWithoutExtension(path).ToLowerInvariant())
115
+ {
116
+ case "jquery.1.7.1":
117
+ case "jquery.tmpl":
118
+ case "includes":
119
+ case "list":
120
+ output = Includes(context, path);
121
+ break;
122
+
123
+ case "results-index":
124
+ output = Index(context);
125
+ break;
126
+
127
+ case "results-list":
128
+ output = ResultList(context);
129
+ break;
130
+
131
+ case "results":
132
+ output = Results(context);
133
+ break;
134
+
135
+ default:
136
+ output = NotFound(context);
137
+ break;
138
+ }
139
+
140
+ context.Response.Write(output);
141
+ }
142
+
143
+ private static string ResultList(HttpContext context)
144
+ {
145
+ string message;
146
+ if (!AuthorizeRequest(context, isList: true, message: out message))
147
+ {
148
+ return message;
149
+ }
150
+
151
+ var lastId = context.Request["last-id"];
152
+ Guid lastGuid = Guid.Empty;
153
+
154
+ if (!lastId.IsNullOrWhiteSpace()) {
155
+ Guid.TryParse(lastId, out lastGuid);
156
+ }
157
+
158
+ var guids = MiniProfiler.Settings.Storage.List(100);
159
+
160
+ if (lastGuid != Guid.Empty)
161
+ {
162
+ guids = guids.TakeWhile(g => g != lastGuid);
163
+ }
164
+
165
+ guids = guids.Reverse();
166
+
167
+ return guids.Select(g =>
168
+ {
169
+ var profiler = MiniProfiler.Settings.Storage.Load(g);
170
+ return new
171
+ {
172
+ profiler.Id,
173
+ profiler.Name,
174
+ profiler.DurationMilliseconds,
175
+ profiler.DurationMillisecondsInSql,
176
+ profiler.ClientTimings,
177
+ profiler.Started,
178
+ profiler.ExecutedNonQueries,
179
+ profiler.ExecutedReaders,
180
+ profiler.ExecutedScalars,
181
+ profiler.HasAllTrivialTimings,
182
+ profiler.HasDuplicateSqlTimings,
183
+ profiler.HasSqlTimings,
184
+ profiler.HasTrivialTimings,
185
+ profiler.HasUserViewed,
186
+ profiler.MachineName,
187
+ profiler.User
188
+ };
189
+ }
190
+
191
+ ).ToJson();
192
+ }
193
+
194
+ private static string Index(HttpContext context)
195
+ {
196
+ string message;
197
+ if (!AuthorizeRequest(context, isList: true, message: out message))
198
+ {
199
+ return message;
200
+ }
201
+
202
+ context.Response.ContentType = "text/html";
203
+
204
+ var path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash();
205
+
206
+ return new StringBuilder()
207
+ .AppendLine("<html><head>")
208
+ .AppendFormat("<title>List of profiling sessions</title>")
209
+ .AppendLine()
210
+ .AppendLine("<script type='text/javascript' src='" + path + "jquery.1.7.1.js?v=" + MiniProfiler.Settings.Version + "'></script>")
211
+ .AppendLine("<script type='text/javascript' src='" + path + "jquery.tmpl.js?v=" + MiniProfiler.Settings.Version + "'></script>")
212
+ .AppendLine("<script type='text/javascript' src='" + path + "includes.js?v=" + MiniProfiler.Settings.Version + "'></script>")
213
+ .AppendLine("<script type='text/javascript' src='" + path + "list.js?v=" + MiniProfiler.Settings.Version + "'></script>")
214
+ .AppendLine("<link href='" + path +"list.css?v=" + MiniProfiler.Settings.Version + "' rel='stylesheet' type='text/css'>")
215
+ .AppendLine("<script type='text/javascript'>MiniProfiler.list.init({path: '" + path + "', version: '" + MiniProfiler.Settings.Version + "'})</script>")
216
+ .AppendLine("</head><body></body></html>")
217
+ .ToString();
218
+ }
219
+
220
+ /// <summary>
221
+ /// Handles rendering static content files.
222
+ /// </summary>
223
+ private static string Includes(HttpContext context, string path)
224
+ {
225
+ var response = context.Response;
226
+
227
+ switch (Path.GetExtension(path))
228
+ {
229
+ case ".js":
230
+ response.ContentType = "application/javascript";
231
+ break;
232
+ case ".css":
233
+ response.ContentType = "text/css";
234
+ break;
235
+ case ".tmpl":
236
+ response.ContentType = "text/x-jquery-tmpl";
237
+ break;
238
+ default:
239
+ return NotFound(context);
240
+ }
241
+
242
+ #if !DEBUG
243
+ var cache = response.Cache;
244
+ cache.SetCacheability(System.Web.HttpCacheability.Public);
245
+ cache.SetExpires(DateTime.Now.AddDays(7));
246
+ cache.SetValidUntilExpires(true);
247
+ #endif
248
+
249
+
250
+ var embeddedFile = Path.GetFileName(path);
251
+ return GetResource(embeddedFile);
252
+ }
253
+
254
+ /// <summary>
255
+ /// Handles rendering a previous MiniProfiler session, identified by its "?id=GUID" on the query.
256
+ /// </summary>
257
+ private static string Results(HttpContext context)
258
+ {
259
+ // when we're rendering as a button/popup in the corner, we'll pass ?popup=1
260
+ // if it's absent, we're rendering results as a full page for sharing
261
+ var isPopup = !string.IsNullOrWhiteSpace(context.Request["popup"]);
262
+
263
+ // this guid is the MiniProfiler.Id property
264
+ Guid id;
265
+ if (!Guid.TryParse(context.Request["id"], out id))
266
+ return isPopup ? NotFound(context) : NotFound(context, "text/plain", "No Guid id specified on the query string");
267
+
268
+ MiniProfiler.Settings.EnsureStorageStrategy();
269
+ var profiler = MiniProfiler.Settings.Storage.Load(id);
270
+
271
+ var provider = WebRequestProfilerProvider.Settings.UserProvider;
272
+ string user = null;
273
+ if (provider != null)
274
+ {
275
+ user = provider.GetUser(context.Request);
276
+ }
277
+
278
+ MiniProfiler.Settings.Storage.SetViewed(user, id);
279
+
280
+ if (profiler == null)
281
+ {
282
+ return isPopup ? NotFound(context) : NotFound(context, "text/plain", "No MiniProfiler results found with Id=" + id.ToString());
283
+ }
284
+
285
+ bool needsSave = false;
286
+ if (profiler.ClientTimings == null)
287
+ {
288
+ profiler.ClientTimings = ClientTimings.FromRequest(context.Request);
289
+ if (profiler.ClientTimings != null)
290
+ {
291
+ needsSave = true;
292
+ }
293
+ }
294
+
295
+ if (profiler.HasUserViewed == false)
296
+ {
297
+ profiler.HasUserViewed = true;
298
+ needsSave = true;
299
+ }
300
+
301
+ if (needsSave) MiniProfiler.Settings.Storage.Save(profiler);
302
+
303
+
304
+ var authorize = MiniProfiler.Settings.Results_Authorize;
305
+
306
+
307
+ if (authorize != null && !authorize(context.Request))
308
+ {
309
+ context.Response.ContentType = "application/json";
310
+ return "hidden".ToJson();
311
+ }
312
+
313
+ return isPopup ? ResultsJson(context, profiler) : ResultsFullPage(context, profiler);
314
+ }
315
+
316
+ private static bool AuthorizeRequest(HttpContext context, bool isList, out string message)
317
+ {
318
+ message = null;
319
+ var authorize = MiniProfiler.Settings.Results_Authorize;
320
+ var authorizeList = MiniProfiler.Settings.Results_List_Authorize;
321
+
322
+ if (authorize != null && !authorize(context.Request) || (isList && (authorizeList == null || !authorizeList(context.Request))))
323
+ {
324
+ context.Response.StatusCode = 401;
325
+ context.Response.ContentType = "text/plain";
326
+ message = "unauthorized";
327
+ return false;
328
+ }
329
+ return true;
330
+ }
331
+
332
+ private static string ResultsJson(HttpContext context, MiniProfiler profiler)
333
+ {
334
+ context.Response.ContentType = "application/json";
335
+ return MiniProfiler.ToJson(profiler);
336
+ }
337
+
338
+ private static string ResultsFullPage(HttpContext context, MiniProfiler profiler)
339
+ {
340
+ context.Response.ContentType = "text/html";
341
+
342
+ var template = GetResource("share.html");
343
+ return template.Format(new
344
+ {
345
+ name = profiler.Name,
346
+ duration = profiler.DurationMilliseconds.ToString(),
347
+ path = VirtualPathUtility.ToAbsolute(MiniProfiler.Settings.RouteBasePath).EnsureTrailingSlash(),
348
+ json = MiniProfiler.ToJson(profiler),
349
+ includes = RenderIncludes(profiler),
350
+ version = MiniProfiler.Settings.Version
351
+ });
352
+ }
353
+
354
+ private static bool bypassLocalLoad = false;
355
+ private static string GetResource(string filename)
356
+ {
357
+ filename = filename.ToLower();
358
+ string result;
359
+
360
+ #if DEBUG
361
+ // attempt to simply load from file system, this lets up modify js without needing to recompile A MILLION TIMES
362
+ if (!bypassLocalLoad)
363
+ {
364
+
365
+ var trace = new System.Diagnostics.StackTrace(true);
366
+ var path = System.IO.Path.GetDirectoryName(trace.GetFrames()[0].GetFileName()) + "\\..\\UI\\" + filename;
367
+ try
368
+ {
369
+ return File.ReadAllText(path);
370
+ }
371
+ catch
372
+ {
373
+ bypassLocalLoad = true;
374
+ }
375
+ }
376
+
377
+ #endif
378
+
379
+ if (!_ResourceCache.TryGetValue(filename, out result))
380
+ {
381
+ string customTemplatesPath = HttpContext.Current.Server.MapPath(MiniProfiler.Settings.CustomUITemplates);
382
+ string customTemplateFile = System.IO.Path.Combine(customTemplatesPath, filename);
383
+
384
+ if (System.IO.File.Exists(customTemplateFile))
385
+ {
386
+ result = File.ReadAllText(customTemplateFile);
387
+ }
388
+ else
389
+ {
390
+ using (var stream = typeof(MiniProfilerHandler).Assembly.GetManifestResourceStream("StackExchange.Profiling.UI." + filename))
391
+ using (var reader = new StreamReader(stream))
392
+ {
393
+ result = reader.ReadToEnd();
394
+ }
395
+ }
396
+ _ResourceCache[filename] = result;
397
+ }
398
+
399
+ return result;
400
+ }
401
+
402
+ /// <summary>
403
+ /// Embedded resource contents keyed by filename.
404
+ /// </summary>
405
+ private static readonly ConcurrentDictionary<string, string> _ResourceCache = new ConcurrentDictionary<string, string>();
406
+
407
+ /// <summary>
408
+ /// Helper method that sets a proper 404 response code.
409
+ /// </summary>
410
+ private static string NotFound(HttpContext context, string contentType = "text/plain", string message = null)
411
+ {
412
+ context.Response.StatusCode = 404;
413
+ context.Response.ContentType = contentType;
414
+
415
+ return message;
416
+ }
417
+
418
+ }
419
+ }