datascope 0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f7decf328918f177ab900bd6a52989a5ed11f943
4
+ data.tar.gz: 570c849040549da1cad3ada61091d45175fa3fc6
5
+ SHA512:
6
+ metadata.gz: 3848caecfa5d7abf4d07b686baedb00d14e0939d7751980e1175e432832da598b4af3afa8b3480ffcedff98752609a7e120c55d9f595e79f01b4b77a329c27e4
7
+ data.tar.gz: eb581fbd5fff872631cdda1b58f95efbb60a21e37ccdf57aac50bf5d6edf8577553cde8398e3a41dcad98b2495002a0403913c9d60498124c632b92d3e83b83b
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ .env
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,46 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ datascope (0.0.1)
5
+ haml
6
+ pg
7
+ puma
8
+ rack-coffee
9
+ sequel
10
+ sequel_pg
11
+ sinatra
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ coffee-script (2.2.0)
17
+ coffee-script-source
18
+ execjs
19
+ coffee-script-source (1.7.0)
20
+ execjs (2.0.2)
21
+ haml (4.0.5)
22
+ tilt
23
+ pg (0.17.1)
24
+ puma (2.8.1)
25
+ rack (>= 1.1, < 2.0)
26
+ rack (1.5.2)
27
+ rack-coffee (1.0.2)
28
+ coffee-script
29
+ rack
30
+ rack-protection (1.5.2)
31
+ rack
32
+ sequel (4.8.0)
33
+ sequel_pg (1.6.9)
34
+ pg (>= 0.8.0)
35
+ sequel (>= 3.39.0)
36
+ sinatra (1.4.4)
37
+ rack (~> 1.4)
38
+ rack-protection (~> 1.4)
39
+ tilt (~> 1.3, >= 1.3.4)
40
+ tilt (1.4.1)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ datascope!
data/Procfile ADDED
@@ -0,0 +1,2 @@
1
+ web: bundle exec puma -t 8:24 -e $RACK_ENV -p $PORT
2
+ worker: bundle exec ruby worker.rb
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/Readme.md ADDED
@@ -0,0 +1,40 @@
1
+ # Datascope
2
+ Visability into your Postgres 9.2 database via [pg_stat_statements](http://www.postgresql.org/docs/9.2/static/pgstatstatements.html) and [cubism](http://square.github.com/cubism/) and using the [json datatype](http://wiki.postgresql.org/wiki/What's_new_in_PostgreSQL_9.2#JSON_datatype).
3
+
4
+ ![http://f.cl.ly/items/440Z1L1n2v3q3c1Q3J0s/datascope.png](http://f.cl.ly/items/440Z1L1n2v3q3c1Q3J0s/datascope.png)
5
+
6
+ Check out a [live example](https://datascope.herokuapp.com)
7
+
8
+ # Heroku Deploy
9
+
10
+ Datascope needs two Postgres 9.2 databases. The first is a DATABASE_URL with the datascope schema, the second is a TARGET_DB with the pg_stat_statements extension.
11
+
12
+ ```bash
13
+ $ heroku create
14
+
15
+ $ heroku addons:add heroku-postgresql:dev --version=9.2
16
+ Attached as HEROKU_POSTGRESQL_COPPER_URL
17
+ $ heroku config:add DATABASE_URL=$(heroku config:get HEROKU_POSTGRESQL_COPPER_URL)
18
+ $ heroku pg:psql COPPER
19
+ => \i schema.sql
20
+ CREATE TABLE
21
+
22
+ $ heroku addons:add heroku-postgresql:dev --version=9.2
23
+ Attached as HEROKU_POSTGRESQL_GREEN_URL
24
+
25
+ $ heroku config:add TARGET_DB=$(heroku config:get HEROKU_POSTGRESQL_GREEN_URL)
26
+ $ heroku pg:psql GREEN
27
+ => create extension pg_stat_statements;
28
+ CREATE EXTENSION
29
+
30
+ $ git push heroku master
31
+ $ heroku scale worker=1
32
+ ```
33
+
34
+ # Basic Auth
35
+
36
+ If you don't want your deployment of datascope to be publicly visible, simply add environment variables for `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD`.
37
+
38
+ ```
39
+ heroku config:add BASIC_AUTH_USER=admin BASIC_AUTH_PASSWORD=password
40
+ ```
data/app.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'sinatra'
2
+ require 'sequel'
3
+ require 'json'
4
+ require 'haml'
5
+ DB = Sequel.connect ENV['DATABASE_URL']
6
+
7
+ class Datascope < Sinatra::Application
8
+
9
+ if (ENV['BASIC_AUTH_USER'] && ENV['BASIC_AUTH_PASSWORD'])
10
+ use Rack::Auth::Basic, "Datascope" do |username, password|
11
+ username == ENV['BASIC_AUTH_USER'] && password == ENV['BASIC_AUTH_PASSWORD']
12
+ end
13
+ end
14
+
15
+ get '/' do
16
+ haml :index
17
+ end
18
+
19
+ get '/metric' do
20
+ selector = params[:selector]
21
+ start = DateTime.parse params[:start]
22
+ stop = DateTime.parse params[:stop]
23
+ step = params[:step].to_i
24
+
25
+ results = DB[:stats].select(:data).filter(created_at: (start..stop)).all
26
+ parsed = results.map{|row| JSON.parse(row[:data])}
27
+
28
+ if selector == 'query_1'
29
+ values = values_by_regex parsed, /with packed/i, :ms
30
+ elsif selector == 'select'
31
+ values = values_by_regex parsed, /select/i
32
+ elsif selector == 'select_ms'
33
+ values = values_by_regex parsed, /select/i, :ms
34
+ elsif selector == 'update'
35
+ values = values_by_regex parsed, /update/i
36
+ elsif selector == 'update_ms'
37
+ values = values_by_regex parsed, /update/i, :ms
38
+ elsif selector == 'insert'
39
+ values = values_by_regex parsed, /insert/i
40
+ elsif selector == 'insert_ms'
41
+ values = values_by_regex parsed, /insert/i, :ms
42
+ elsif selector == 'delete'
43
+ values = values_by_regex parsed, /delete/i
44
+ else
45
+ values = parsed.map{|d| d[selector] }
46
+ end
47
+
48
+ JSON.dump values
49
+ end
50
+
51
+ get '/queries' do
52
+ data = JSON.parse DB[:stats]
53
+ .select(:data)
54
+ .order_by(:id.desc)
55
+ .first[:data]
56
+ JSON.dump data['stat_statements'].map{|s| s['query'] = s['query'][0..50]; s}.sort{|s| s['total_time']}.reverse
57
+ end
58
+
59
+ def values_by_regex(parsed, regex, ms=false)
60
+ vals = parsed.map { |row|
61
+ row['stat_statements']
62
+ .select {|h| h['query'] =~ regex }
63
+ .sort_by {|h| h['total_time']}
64
+ .inject([0,0]) { |m,h| [m.first + h['calls'].to_i, m.last + h['total_time'].to_f ] }
65
+ }
66
+
67
+ if ms
68
+ vals.map {|pair| pair.first.zero? ? 0 : pair.last/pair.first}
69
+ else
70
+ vals.map(&:first)
71
+ end
72
+ end
73
+
74
+ end
data/config.ru ADDED
@@ -0,0 +1,7 @@
1
+ require './app'
2
+ require 'rack/coffee'
3
+
4
+ use Rack::Coffee
5
+
6
+ run Datascope
7
+
data/datascope.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "datascope"
7
+ gem.version = "0.0.1"
8
+ gem.authors = ["Will Leinweber"]
9
+ gem.email = []
10
+ gem.description = %q{postgres 9.2 visibility}
11
+ gem.summary = %q{postgres 9.2 visibility}
12
+ gem.homepage = "https://github.com/will/datascope"
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(spec)/})
17
+ gem.require_paths = ["lib"]
18
+
19
+ ##
20
+ # Runtime dependencies.
21
+
22
+ gem.add_runtime_dependency "sinatra"
23
+ gem.add_runtime_dependency "puma"
24
+ gem.add_runtime_dependency "pg"
25
+ gem.add_runtime_dependency "sequel"
26
+ gem.add_runtime_dependency "sequel_pg"
27
+ gem.add_runtime_dependency "haml"
28
+ gem.add_runtime_dependency "rack-coffee"
29
+ end
30
+
@@ -0,0 +1,55 @@
1
+ context = cubism.context()
2
+ .step(1e4)
3
+ .size(960)
4
+
5
+ getThing = (name, selector) ->
6
+ context.metric( (start,stop,step,callback)->
7
+ url = "/metric?selector=#{selector}&start=#{start.toISOString()}&stop=#{stop.toISOString()}&step=#{step}"
8
+ d3.json url, (data) ->
9
+ return callback(new Error('could not load data')) unless data
10
+ callback(null, data)
11
+
12
+ , name)
13
+
14
+
15
+
16
+ $.getJSON('/queries', (data) ->
17
+ console.log(data)
18
+ $ -> _.each(data, (it) -> $('#queries').append("<li><em>#{it.calls}c, #{it.total_time}ms</em> #{it.query}</li>"))
19
+ )
20
+
21
+ $ ->
22
+ d3.select("#cubism").selectAll(".axis")
23
+ .data(["top", "bottom"])
24
+ .enter().append("div")
25
+ .attr("class", (d)-> return d + " axis" )
26
+ .each((d) -> d3.select(this).call(context.axis().ticks(12).orient(d)))
27
+
28
+ d3.select("#cubism").append("div")
29
+ .attr("class", "rule")
30
+ .call(context.rule())
31
+
32
+ d3.select("#cubism").selectAll(".horizon")
33
+ .data([
34
+ getThing('conn count' , 'connections') ,
35
+ getThing('cache hit' , 'cache_hit') ,
36
+ getThing('SELECT (count)' , 'select') ,
37
+ getThing('SELECT (ms)' , 'select_ms') ,
38
+ getThing('UPDATE (count)' , 'update') ,
39
+ getThing('UPDATE (ms)' , 'update_ms') ,
40
+ getThing('INSERT (count)' , 'insert') ,
41
+ getThing('INSERT (ms)' , 'insert_ms') ,
42
+ getThing('locks' , 'locks') ,
43
+ getThing('voodoo query (ms)', 'query_1') ,
44
+ ])
45
+ .enter().insert("div", ".bottom")
46
+ .attr("class", "horizon")
47
+ .call(context.horizon().height(60)) #.extent([0, 15]))
48
+
49
+ context.on("focus", (i) ->
50
+ d3.selectAll(".value").style("right", i == null ? null : context.size() - i + "px")
51
+ )
52
+
53
+
54
+
55
+
@@ -0,0 +1,53 @@
1
+ require 'sequel'
2
+ require 'json'
3
+ DB = Sequel.connect ENV['DATABASE_URL']
4
+ TARGET_DB = Sequel.connect ENV['TARGET_DB']
5
+
6
+ module StatCollector
7
+ extend self
8
+
9
+ def stats
10
+ {
11
+ connections: connections,
12
+ stat_statements: stat_statements,
13
+ cache_hit: cache_hit,
14
+ locks: locks
15
+ }
16
+ end
17
+
18
+ def capture_stats!
19
+ s = stats
20
+ DB[:stats] << {data: s.to_json}
21
+ "connections=#{s[:connections]} stat_statements=#{s[:stat_statements].count} cache_hit=#{cache_hit}"
22
+ end
23
+
24
+ def reset_target_stats!
25
+ target_db.execute "select pg_stat_statements_reset()"
26
+ end
27
+
28
+ def target_db
29
+ TARGET_DB
30
+ end
31
+
32
+ def connections
33
+ target_db[:pg_stat_activity].count
34
+ end
35
+
36
+ def stat_statements
37
+ TARGET_DB[:pg_stat_statements]
38
+ .select(:query, :calls, :total_time)
39
+ .exclude(query: '<insufficient privilege>')
40
+ .all
41
+ end
42
+
43
+ def cache_hit
44
+ target_db[:pg_statio_user_tables]
45
+ .select("(sum(heap_blks_hit) - sum(heap_blks_read)) / sum(heap_blks_hit) as ratio".lit)
46
+ .first[:ratio]
47
+ .to_f
48
+ end
49
+
50
+ def locks
51
+ target_db[:pg_locks].exclude(:granted).count
52
+ end
53
+ end
@@ -0,0 +1,103 @@
1
+ body {
2
+ font-family: "Helvetica Neue", Helvetica, sans-serif;
3
+ margin: 30px auto;
4
+ width: 960px;
5
+ position: relative;
6
+ }
7
+
8
+ header {
9
+ padding: 6px 0;
10
+ }
11
+
12
+ .group {
13
+ margin-bottom: 1em;
14
+ }
15
+
16
+ .axis {
17
+ font: 10px sans-serif;
18
+ position: fixed;
19
+ pointer-events: none;
20
+ z-index: 2;
21
+ }
22
+
23
+ .axis text {
24
+ -webkit-transition: fill-opacity 250ms linear;
25
+ }
26
+
27
+ .axis path {
28
+ display: none;
29
+ }
30
+
31
+ .axis line {
32
+ stroke: #000;
33
+ shape-rendering: crispEdges;
34
+ }
35
+
36
+ .axis.top {
37
+ background-image: linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
38
+ background-image: -o-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
39
+ background-image: -moz-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
40
+ background-image: -webkit-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
41
+ background-image: -ms-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
42
+ top: 0px;
43
+ padding: 0 0 24px 0;
44
+ }
45
+
46
+ .axis.bottom {
47
+ background-image: linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
48
+ background-image: -o-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
49
+ background-image: -moz-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
50
+ background-image: -webkit-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
51
+ background-image: -ms-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
52
+ bottom: 0px;
53
+ padding: 24px 0 0 0;
54
+ }
55
+
56
+ .horizon {
57
+ border-bottom: solid 1px #000;
58
+ overflow: hidden;
59
+ position: relative;
60
+ }
61
+
62
+ .horizon {
63
+ border-top: solid 1px #000;
64
+ border-bottom: solid 1px #000;
65
+ }
66
+
67
+ .horizon + .horizon {
68
+ border-top: none;
69
+ }
70
+
71
+ .horizon canvas {
72
+ display: block;
73
+ }
74
+
75
+ .horizon .title,
76
+ .horizon .value {
77
+ bottom: 0;
78
+ line-height: 30px;
79
+ margin: 0 6px;
80
+ position: absolute;
81
+ text-shadow: 0 1px 0 rgba(255,255,255,.5);
82
+ white-space: nowrap;
83
+ }
84
+
85
+ .horizon .title {
86
+ left: 0;
87
+ }
88
+
89
+ .horizon .value {
90
+ right: 0;
91
+ }
92
+
93
+ .line {
94
+ background: #000;
95
+ opacity: .2;
96
+ z-index: 2;
97
+ }
98
+
99
+ @media all and (max-width: 1439px) {
100
+ body { margin: 0px auto; }
101
+ .axis { position: static; }
102
+ .axis.top, .axis.bottom { padding: 0; }
103
+ }
@@ -0,0 +1,38 @@
1
+ // Backbone.js 0.9.2
2
+
3
+ // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
4
+ // Backbone may be freely distributed under the MIT license.
5
+ // For all details and documentation:
6
+ // http://backbonejs.org
7
+ (function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
8
+ {});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
9
+ z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
10
+ {};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
11
+ b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
12
+ b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
13
+ a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
14
+ h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
15
+ return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
16
+ {};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
17
+ !this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
18
+ this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
19
+ l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
20
+ a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
21
+ shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
22
+ this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
23
+ e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
24
+ {};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
25
+ arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
26
+ C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
27
+ this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
28
+ ""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
29
+ !(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
30
+ this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
31
+ stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
32
+ function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
33
+ this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
34
+ f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
35
+ for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
36
+ !1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
37
+ e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
38
+ b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);