├── stress ├── __init__.py ├── static │ ├── img │ │ ├── ajax-loader.gif │ │ ├── background.png │ │ ├── glyphicons-halflings.png │ │ └── glyphicons-halflings-white.png │ ├── js │ │ ├── bootstrap-dropdown.js │ │ ├── bootstrap-popover.js │ │ ├── query_graphs.js │ │ ├── bootstrap-tab.js │ │ ├── metrics_graphs.js │ │ ├── live.js │ │ ├── query_dial.js │ │ ├── bootstrap-modal.js │ │ ├── stress.js │ │ ├── mysql.js │ │ ├── bootstrap-tooltip.js │ │ ├── common.js │ │ └── jquery.knob-1.1.1.js │ ├── lang │ │ └── sh_sql.min.js │ └── css │ │ ├── codemirror.css │ │ ├── stress.css │ │ ├── stress.scss │ │ ├── bootstrap-responsive.min.css │ │ └── bootstrap-responsive.css ├── test_worker.py ├── utils.py ├── plancache.py ├── query_table.py ├── worker.py ├── database.py ├── templates │ └── index.html └── server.py ├── .gitignore ├── runner ├── .arcconfig ├── workloads ├── key_value │ ├── key_value.sql │ └── key_value.json └── video │ ├── video.json │ └── video.sql └── README.md /stress/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.data 4 | .sass-cache 5 | -------------------------------------------------------------------------------- /runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import stress.server 4 | stress.server.main() 5 | -------------------------------------------------------------------------------- /.arcconfig: -------------------------------------------------------------------------------- 1 | { 2 | "project_id" : "workloadsimulator", 3 | "conduit_uri" : "http:\/\/grizzly.memsql.com\/api\/" 4 | } 5 | -------------------------------------------------------------------------------- /stress/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memsql/workload-simulator/HEAD/stress/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /stress/static/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memsql/workload-simulator/HEAD/stress/static/img/background.png -------------------------------------------------------------------------------- /stress/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memsql/workload-simulator/HEAD/stress/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /stress/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/memsql/workload-simulator/HEAD/stress/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /workloads/key_value/key_value.sql: -------------------------------------------------------------------------------- 1 | create database if not exists key_value; 2 | use key_value; 3 | 4 | DROP TABLE IF EXISTS `kv`; 5 | CREATE TABLE `kv` ( 6 | `k` bigint(20) NOT NULL, 7 | `v` varbinary(64) DEFAULT NULL, 8 | PRIMARY KEY (`k`) USING HASH 9 | ); 10 | -------------------------------------------------------------------------------- /workloads/key_value/key_value.json: -------------------------------------------------------------------------------- 1 | {"workload": {"1": {"query": "select v from kv \n\twhere k=@", "qps": "52728"}, "0": {"query": "insert into kv \n\tvalues (@, ^)", "qps": "40939"}, "3": {"query": "delete from kv \n\twhere k=@", "qps": "37701"}, "2": {"query": "update kv set v=^\n\twhere k=@", "qps": "42868"}}, "settings": {"memsql_host": "127.0.0.1", "memsql_db": "key_value", "memsql_port": "3306", "qps-number": "100000", "workers": "50", "memsql_pass": "", "memsql_user": "root", "dial_max_value": 100000}} -------------------------------------------------------------------------------- /stress/test_worker.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import worker 4 | 5 | client_arguments = dict(host = '127.0.0.1', port = 3306, user = 'root', passwd = '', db = 'test') 6 | 7 | workload = { 8 | 0 : { 9 | 'query' : 'select * from x where id > @', 10 | 'qps' : 1000 11 | } 12 | } 13 | 14 | 15 | if __name__ == '__main__': 16 | pool = worker.WorkerPool(10, client_arguments) 17 | assert pool.is_alive() 18 | pool.send_workload(workload) 19 | time.sleep(10) 20 | print "--> WOKE UP" 21 | assert pool.is_alive() 22 | print "--> ABOUT TO PAUSE" 23 | pool.pause() 24 | print "--> DONE PAUSING" 25 | -------------------------------------------------------------------------------- /workloads/video/video.json: -------------------------------------------------------------------------------- 1 | {"workload": {"1": {"query": "UPDATE order_line SET ol_delivery_d = @ WHERE ol_o_id = @ AND ol_d_id = @ AND ol_w_id = @", "qps": "15000"}, "0": {"query": "INSERT INTO orders (o_id, o_d_id, o_w_id, o_c_id, o_entry_d, o_ol_cnt, o_all_local) VALUES (@, @, @, @, @, @, @)", "qps": "15000"}, "3": {"query": "INSERT INTO customer values(@, @, @, @, @, @, @, @, @, @, @, @, @, @, @, @, @, @, @, @, @)", "qps": "15000"}, "2": {"query": "SELECT count(c_id) FROM customer WHERE c_w_id = @ AND c_d_id = @ AND c_last = @", "qps": "15000"}, "5": {"query": "select * from orders where o_w_id = @", "qps": "15000"}, "4": {"query": "UPDATE customer SET c_balance = @ WHERE c_w_id = @ AND c_d_id = @ AND c_id = @", "qps": "15000"}}, "settings": {"memsql_host": "127.0.0.1", "memsql_db": "video", "memsql_port": "3306", "qps-number": "15000", "workers": "50", "memsql_pass": "", "memsql_user": "root", "dial_max_value": 15000}} -------------------------------------------------------------------------------- /stress/utils.py: -------------------------------------------------------------------------------- 1 | """Helper functions. 2 | """ 3 | 4 | import logging 5 | import optparse 6 | import database 7 | from sqlalchemy import pool 8 | 9 | # Some basic configuration 10 | LOGGING_FORMAT = '%(levelname)s: %(asctime)-15s: %(message)s' 11 | 12 | # Uniform interface for parsing options, with some common built-in options. 13 | options = None 14 | largs = None 15 | 16 | parser = optparse.OptionParser(add_help_option=False) 17 | parser.add_option("--memsql-host", help="memsql hostname", default="127.0.0.1") 18 | parser.add_option("--memsql-port", help="memsql port", type="int", default=3306) 19 | parser.add_option("--memsql-user", help="memsql user", default="root") 20 | parser.add_option("--memsql-pass", help="memsql pass", default="") 21 | parser.add_option("--memsql-db", help="memsql database", default="") 22 | parser.add_option("-w", "--workers", help="default number of workers", type="int", default=50) 23 | parser.add_option("-p", "--server-port", type="int", default=9000, help="server port") 24 | 25 | parser.add_option("", "--help", action="help") 26 | 27 | def _parse_options(): 28 | global options 29 | global largs 30 | if not (options and largs): 31 | options, largs = parser.parse_args() 32 | 33 | def get_options(): 34 | """Return the parsed (named) options.""" 35 | global options 36 | _parse_options() 37 | return options 38 | 39 | def get_largs(): 40 | """Return the parsed (free) options.""" 41 | global largs 42 | _parse_options() 43 | return largs 44 | 45 | # Wraps SQLAlchemy's DB Pool into our own connection pool. 46 | db_pool = pool.manage(database) 47 | def get_db_conn(host=None, port=None, user=None, password=None, database=None): 48 | """Returns a database connection from the connection pool.""" 49 | global db_pool 50 | assert options 51 | 52 | if host is None: 53 | host = options.memsql_host 54 | if port is None: 55 | port = options.memsql_port 56 | if user is None: 57 | user = options.memsql_user 58 | if password is None: 59 | password = options.memsql_password 60 | if database is None: 61 | database = options.memsql_db 62 | 63 | return db_pool.connect( 64 | host='%s:%d' % (host, port), 65 | user=user, password=password, database=database) 66 | 67 | # Sets up the global logger. 68 | logger = logging.basicConfig(level=logging.ERROR, format=LOGGING_FORMAT) 69 | logger = logging 70 | -------------------------------------------------------------------------------- /stress/static/js/bootstrap-dropdown.js: -------------------------------------------------------------------------------- 1 | /* ============================================================ 2 | * bootstrap-dropdown.js v2.0.3 3 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns 4 | * ============================================================ 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * ============================================================ */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* DROPDOWN CLASS DEFINITION 27 | * ========================= */ 28 | 29 | var toggle = '[data-toggle="dropdown"]' 30 | , Dropdown = function (element) { 31 | var $el = $(element).on('click.dropdown.data-api', this.toggle) 32 | $('html').on('click.dropdown.data-api', function () { 33 | $el.parent().removeClass('open') 34 | }) 35 | } 36 | 37 | Dropdown.prototype = { 38 | 39 | constructor: Dropdown 40 | 41 | , toggle: function (e) { 42 | var $this = $(this) 43 | , $parent 44 | , selector 45 | , isActive 46 | 47 | if ($this.is('.disabled, :disabled')) return 48 | 49 | selector = $this.attr('data-target') 50 | 51 | if (!selector) { 52 | selector = $this.attr('href') 53 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 54 | } 55 | 56 | $parent = $(selector) 57 | $parent.length || ($parent = $this.parent()) 58 | 59 | isActive = $parent.hasClass('open') 60 | 61 | clearMenus() 62 | 63 | if (!isActive) $parent.toggleClass('open') 64 | 65 | return false 66 | } 67 | 68 | } 69 | 70 | function clearMenus() { 71 | $(toggle).parent().removeClass('open') 72 | } 73 | 74 | 75 | /* DROPDOWN PLUGIN DEFINITION 76 | * ========================== */ 77 | 78 | $.fn.dropdown = function (option) { 79 | return this.each(function () { 80 | var $this = $(this) 81 | , data = $this.data('dropdown') 82 | if (!data) $this.data('dropdown', (data = new Dropdown(this))) 83 | if (typeof option == 'string') data[option].call($this) 84 | }) 85 | } 86 | 87 | $.fn.dropdown.Constructor = Dropdown 88 | 89 | 90 | /* APPLY TO STANDARD DROPDOWN ELEMENTS 91 | * =================================== */ 92 | 93 | $(function () { 94 | $('html').on('click.dropdown.data-api', clearMenus) 95 | $('body') 96 | .on('click.dropdown', '.dropdown form', function (e) { e.stopPropagation() }) 97 | .on('click.dropdown.data-api', toggle, Dropdown.prototype.toggle) 98 | }) 99 | 100 | }(window.jQuery); -------------------------------------------------------------------------------- /stress/plancache.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import logging 3 | import multiprocessing 4 | import time 5 | 6 | import worker 7 | import utils 8 | 9 | logger = logging.getLogger('plancache') 10 | logger.setLevel(logging.DEBUG) 11 | 12 | def save_plancache_loop(SettingsClass, stats, metrics, lock): 13 | POLLING_PERIOD = worker.PERIOD*10 14 | prev_time = 0 15 | prev_pc = {} 16 | 17 | pc_time = 0 18 | pc_counter = 0 19 | 20 | conn = SettingsClass.get_db_conn() 21 | 22 | while True: 23 | current_time = time.time() 24 | rows_pc = conn.query('show plancache') 25 | rows_mets = conn.query('show status extended') 26 | pc_time += time.time() - current_time 27 | pc_counter += 1 28 | with lock: 29 | for row in rows_pc: # fetch plancache 30 | query = row.QueryText 31 | db = row.Database 32 | 33 | if db != SettingsClass.memsql_db: 34 | continue 35 | 36 | commits = row.Commits if row.Commits is not None else 0 37 | rollbacks = row.Rollbacks if row.Rollbacks is not None else 0 38 | 39 | execs = commits + rollbacks 40 | 41 | if query in prev_pc: 42 | if query in stats or execs != prev_pc[query]: 43 | stats[query] = \ 44 | (execs - prev_pc[query]) * 1.0/(current_time - prev_time) 45 | 46 | prev_pc[query] = execs 47 | 48 | for row in rows_mets: # fetch db metrics 49 | var_name = row.Variable_name 50 | var_val = row.Value 51 | metrics[var_name] = var_val 52 | 53 | logger.debug("Total time spent [%g] Average Time Spent [%g]" % (pc_time, pc_time/pc_counter)) 54 | 55 | prev_time = current_time 56 | time.sleep(POLLING_PERIOD) 57 | 58 | 59 | g_plancaches = {}; 60 | 61 | def plancacheFactory(settings): 62 | if not g_plancaches.has_key(settings): 63 | g_plancaches[settings] = PlancacheStats(settings) 64 | return g_plancaches[settings]; 65 | 66 | class PlancacheBroken(Exception): 67 | pass 68 | 69 | class PlancacheStats(object): 70 | def __init__(self, SettingsClass): 71 | self.manager = multiprocessing.Manager() 72 | self.pc_dict = self.manager.dict() 73 | self.metrics = self.manager.dict() 74 | self.pc_lock = self.manager.Lock() 75 | self.pc_proc = multiprocessing.Process(target=save_plancache_loop, args=(SettingsClass, self.pc_dict, self.metrics, self.pc_lock)) 76 | self.pc_proc.start() 77 | 78 | def __del__(self): 79 | self.pc_proc.terminate() 80 | 81 | def get_metrics(self): 82 | if not self.pc_proc.is_alive(): 83 | raise PlancacheBroken 84 | try: 85 | with self.pc_lock: 86 | return self.metrics.copy() 87 | except IOError as e: 88 | raise PlancacheBroken 89 | 90 | def get_stats(self): 91 | if not self.pc_proc.is_alive(): 92 | raise PlancacheBroken 93 | try: 94 | with self.pc_lock: 95 | return self.pc_dict.copy() 96 | except IOError as e: 97 | raise PlancacheBroken 98 | -------------------------------------------------------------------------------- /stress/static/js/bootstrap-popover.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * bootstrap-popover.js v2.0.4 3 | * http://twitter.github.com/bootstrap/javascript.html#popovers 4 | * =========================================================== 5 | * Copyright 2012 Twitter, Inc. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | * =========================================================== */ 19 | 20 | 21 | !function ($) { 22 | 23 | "use strict"; // jshint ;_; 24 | 25 | 26 | /* POPOVER PUBLIC CLASS DEFINITION 27 | * =============================== */ 28 | 29 | var Popover = function ( element, options ) { 30 | this.init('popover', element, options) 31 | } 32 | 33 | 34 | /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js 35 | ========================================== */ 36 | 37 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, { 38 | 39 | constructor: Popover 40 | 41 | , setContent: function () { 42 | var $tip = this.tip() 43 | , title = this.getTitle() 44 | , content = this.getContent() 45 | 46 | $tip.find('.popover-title')[this.isHTML(title) ? 'html' : 'text'](title) 47 | $tip.find('.popover-content > *')[this.isHTML(content) ? 'html' : 'text'](content) 48 | 49 | $tip.removeClass('fade top bottom left right in') 50 | } 51 | 52 | , hasContent: function () { 53 | return this.getTitle() || this.getContent() 54 | } 55 | 56 | , getContent: function () { 57 | var content 58 | , $e = this.$element 59 | , o = this.options 60 | 61 | content = $e.attr('data-content') 62 | || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content) 63 | 64 | return content 65 | } 66 | 67 | , tip: function () { 68 | if (!this.$tip) { 69 | this.$tip = $(this.options.template) 70 | } 71 | return this.$tip 72 | } 73 | 74 | }) 75 | 76 | 77 | /* POPOVER PLUGIN DEFINITION 78 | * ======================= */ 79 | 80 | $.fn.popover = function (option) { 81 | return this.each(function () { 82 | var $this = $(this) 83 | , data = $this.data('popover') 84 | , options = typeof option == 'object' && option 85 | if (!data) $this.data('popover', (data = new Popover(this, options))) 86 | if (typeof option == 'string') data[option]() 87 | }) 88 | } 89 | 90 | $.fn.popover.Constructor = Popover 91 | 92 | $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, { 93 | placement: 'right' 94 | , content: '' 95 | , template: '

' 96 | }) 97 | 98 | }(window.jQuery); -------------------------------------------------------------------------------- /stress/static/lang/sh_sql.min.js: -------------------------------------------------------------------------------- 1 | if(!this.sh_languages){this.sh_languages={}}sh_languages.sql=[[[/\b(?:VARCHAR|TINYINT|TEXT|DATE|SMALLINT|MEDIUMINT|INT|BIGINT|FLOAT|DOUBLE|DECIMAL|DATETIME|TIMESTAMP|TIME|YEAR|UNSIGNED|CHAR|TINYBLOB|TINYTEXT|BLOB|MEDIUMBLOB|MEDIUMTEXT|LONGBLOB|LONGTEXT|ENUM|BOOL|BINARY|VARBINARY)\b/gi,"sh_type",-1],[/\b(?:ALL|ASC|AS|ALTER|AND|ADD|AUTO_INCREMENT|BETWEEN|BINARY|BOTH|BY|BOOLEAN|CHANGE|CHECK|COLUMNS|COLUMN|CROSS|CREATE|DATABASES|DATABASE|DATA|DELAYED|DESCRIBE|DESC|DISTINCT|DELETE|DROP|DEFAULT|ENCLOSED|ESCAPED|EXISTS|EXPLAIN|FIELDS|FIELD|FLUSH|FOR|FOREIGN|FUNCTION|FROM|GROUP|GRANT|HAVING|IGNORE|INDEX|INFILE|INSERT|INNER|INTO|IDENTIFIED|IN|IS|IF|JOIN|KEYS|KILL|KEY|LEADING|LIKE|LIMIT|LINES|LOAD|LOCAL|LOCK|LOW_PRIORITY|LEFT|LANGUAGE|MODIFY|NATURAL|NOT|NULL|NEXTVAL|OPTIMIZE|OPTION|OPTIONALLY|ORDER|OUTFILE|OR|OUTER|ON|PROCEDURE|PROCEDURAL|PRIMARY|READ|REFERENCES|REGEXP|RENAME|REPLACE|RETURN|REVOKE|RLIKE|RIGHT|SHOW|SONAME|STATUS|STRAIGHT_JOIN|SELECT|SETVAL|SET|TABLES|TERMINATED|TO|TRAILING|TRUNCATE|TABLE|TEMPORARY|TRIGGER|TRUSTED|UNIQUE|UNLOCK|USE|USING|UPDATE|VALUES|VARIABLES|VIEW|WITH|WRITE|WHERE|ZEROFILL|TYPE|XOR)\b/gi,"sh_keyword",-1],[/"/g,"sh_string",1],[/'/g,"sh_string",2],[/`/g,"sh_string",3],[/#/g,"sh_comment",4],[/\/\/\//g,"sh_comment",5],[/\/\//g,"sh_comment",4],[/\/\*\*/g,"sh_comment",11],[/\/\*/g,"sh_comment",12],[/--/g,"sh_comment",4],[/~|!|%|\^|\*|\(|\)|-|\+|=|\[|\]|\\|:|;|,|\.|\/|\?|&|<|>|\|/g,"sh_symbol",-1],[/\b[+-]?(?:(?:0x[A-Fa-f0-9]+)|(?:(?:[\d]*\.)?[\d]+(?:[eE][+-]?[\d]+)?))u?(?:(?:int(?:8|16|32|64))|L)?\b/g,"sh_number",-1]],[[/"/g,"sh_string",-2],[/\\./g,"sh_specialchar",-1]],[[/'/g,"sh_string",-2],[/\\./g,"sh_specialchar",-1]],[[/`/g,"sh_string",-2],[/\\./g,"sh_specialchar",-1]],[[/$/g,null,-2]],[[/$/g,null,-2],[/(?:?)|(?:?)/g,"sh_url",-1],[/<\?xml/g,"sh_preproc",6,1],[//g,"sh_keyword",-1],[/<(?:\/)?[A-Za-z](?:[A-Za-z0-9_:.-]*)/g,"sh_keyword",10,1],[/&(?:[A-Za-z0-9]+);/g,"sh_preproc",-1],[/<(?:\/)?[A-Za-z][A-Za-z0-9]*(?:\/)?>/g,"sh_keyword",-1],[/<(?:\/)?[A-Za-z][A-Za-z0-9]*/g,"sh_keyword",10,1],[/@[A-Za-z]+/g,"sh_type",-1],[/(?:TODO|FIXME|BUG)(?:[:]?)/g,"sh_todo",-1]],[[/\?>/g,"sh_preproc",-2],[/([^=" \t>]+)([ \t]*)(=?)/g,["sh_type","sh_normal","sh_symbol"],-1],[/"/g,"sh_string",7]],[[/\\(?:\\|")/g,null,-1],[/"/g,"sh_string",-2]],[[/>/g,"sh_preproc",-2],[/([^=" \t>]+)([ \t]*)(=?)/g,["sh_type","sh_normal","sh_symbol"],-1],[/"/g,"sh_string",7]],[[/-->/g,"sh_comment",-2],[/ 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 43 | 44 | {% if live %} 45 | 46 | {% else %} 47 | 48 | {% endif %} 49 | 50 | 51 | 52 |
53 |
54 |
55 | Settings 56 | 57 | {% if not live %} 58 | Save Workload 59 | Load Workload 60 | {% endif %} 61 | 62 | 112 | 113 |
114 | 115 |
116 |
Workload Simulator
117 |
118 |
119 |
120 | {% if live %} 121 |

Live Mode

122 | {% else %} 123 |

PLAY

124 |

CLEAR

125 | {% endif %} 126 |
127 |
128 |
129 | 133 | 136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | 146 | 147 | {% if liveNOTWORKING %} 148 | 152 | {% endif %} 153 | 154 |
155 |
156 |
157 |
158 | {% if live %} 159 |

Sort Queries

160 | {% else %} 161 |

Instructions: Enter SQL queries in the boxes below. To generate random input, use @ for numbers and ^ for strings (no quotation marks necessary). Adjust the dial to set the number of queries per second sent to the server.

162 | {% endif %} 163 |
164 | 177 | 178 |
179 |
+
180 |
181 |
182 |
183 |
184 |
185 | {% if live %} 186 |
187 |
188 |
189 | {% endif %} 190 |
191 |
192 | 193 | {% if liveNOTWORKING %} 194 |
195 |
196 |
197 | {% endif %} 198 |
199 |
200 | 201 |
202 | 203 | 216 | 217 | 229 | 230 |
231 | 237 |
238 | 239 | 251 | 252 | 253 | 254 | -------------------------------------------------------------------------------- /stress/server.py: -------------------------------------------------------------------------------- 1 | """Add a description here 2 | """ 3 | 4 | from flask import Flask, request, session, g, redirect, url_for, \ 5 | abort, render_template, flash, make_response 6 | 7 | from cStringIO import StringIO 8 | import logging 9 | import multiprocessing 10 | import re 11 | import subprocess 12 | import sys 13 | import _mysql 14 | import string 15 | 16 | import database 17 | import plancache 18 | import query_table 19 | import utils 20 | import worker 21 | 22 | import random 23 | from datetime import datetime, timedelta 24 | 25 | try: 26 | import simplejson as json 27 | except ImportError as e: 28 | import json 29 | 30 | logger = logging.getLogger('server') 31 | logger.setLevel(logging.DEBUG) 32 | 33 | DEBUG = True 34 | SECRET_KEY = 'foobar' 35 | PING_TIMEOUT = 10 # in seconds 36 | VERY_LONG_AGO = datetime(1990, 03, 02) # send me a bday card 37 | 38 | app = Flask(__name__) 39 | app.config.from_object(__name__) 40 | 41 | g_workload = None 42 | g_workers = None 43 | g_last_ping = VERY_LONG_AGO 44 | g_settings = None 45 | 46 | # immutable, at least in theory 47 | class Settings: 48 | def __init__(self, d): 49 | self.memsql_host = None 50 | self.memsql_port = None 51 | self.memsql_user = None 52 | self.memsql_pass = None 53 | self.memsql_db = None 54 | self.workers = None 55 | change = False 56 | try: 57 | for key, setting in d.items(): 58 | if not hasattr(self, key): 59 | continue 60 | if isinstance(setting, str): 61 | setting = setting.lstrip().rstrip() 62 | if key in ['memsql_port', 'workers']: 63 | setting = int(setting) 64 | if getattr(self, key) != setting: 65 | change = True 66 | setattr(self, key, setting) 67 | except ValueError as e: 68 | raise ServerException('Invalid settings. Please verify port number and workers processes', ServerException.ER_SETTINGS) 69 | 70 | def __eq__(self, another): 71 | return hasattr(another, 'memsql_host') and self.memsql_host == another.memsql_host \ 72 | and hasattr(another, 'memsql_port') and self.memsql_port == another.memsql_port \ 73 | and hasattr(another, 'memsql_user') and self.memsql_user == another.memsql_user \ 74 | and hasattr(another, 'memsql_pass') and self.memsql_pass == another.memsql_pass \ 75 | and hasattr(another, 'memsql_db') and self.memsql_db == another.memsql_db \ 76 | and hasattr(another, 'workers') and self.workers == another.workers 77 | 78 | def __hash__(self): 79 | return hash(frozenset(self.get_dict().items())); 80 | 81 | def get_db_conn(self): 82 | return utils.get_db_conn(self.memsql_host, self.memsql_port, \ 83 | self.memsql_user, self.memsql_pass, self.memsql_db) 84 | 85 | def get_dict(self): 86 | return { 87 | 'memsql_host' : self.memsql_host, 88 | 'memsql_port' : self.memsql_port, 89 | 'memsql_user' : self.memsql_user, 90 | 'memsql_pass' : self.memsql_pass, 91 | 'memsql_db' : self.memsql_db, 92 | 'workers' : self.workers 93 | } 94 | 95 | def get_client_arguments(self): 96 | return { 97 | 'host' : self.memsql_host, 98 | 'port' : self.memsql_port, 99 | 'user' : self.memsql_user, 100 | 'passwd' : self.memsql_pass, 101 | 'db' : self.memsql_db, 102 | 'unix_socket' : '/tmp/memsql.sock' 103 | } 104 | 105 | class ServerException(Exception): 106 | ER_UNKNOWN = 0 107 | ER_DBCONN = 1 108 | ER_DBNAME = 2 109 | ER_JS = 3 110 | ER_QUERY = 4 111 | ER_SETTINGS = 5 112 | 113 | def __init__(self, message, n=ER_UNKNOWN): 114 | self.message = message 115 | self.n = n 116 | 117 | def to_dict(self): 118 | return {'errno' : self.n, 'message' : self.message} 119 | 120 | def __str__(self): 121 | return "[%d] %s" % self.message 122 | 123 | class WorkloadException(ServerException): 124 | def __init__(self, id_to_error): 125 | self.id_to_error = id_to_error 126 | 127 | def to_dict(self): 128 | ret = { 129 | 'errno' : ServerException.ER_QUERY, 130 | 'message' : 'Invalid queries in workload', 131 | 'query_map' : self.id_to_error 132 | } 133 | return ret 134 | 135 | def format_response(running, error=None, **kwargs): 136 | ret = {'running' : running} 137 | if error: 138 | assert isinstance(error, ServerException) 139 | ret['error'] = error.to_dict() 140 | 141 | ret.update(kwargs) 142 | return json.dumps(ret) 143 | 144 | def validate_workload(): 145 | global g_workload 146 | global g_workers 147 | global g_settings 148 | 149 | try: 150 | conn = _mysql.connect(**g_settings.get_client_arguments()) 151 | 152 | try: 153 | qt = query_table.QueryTable(g_workload, g_workers.qps_array, g_workers.qps_query_table) 154 | failed_queries = {} 155 | for q in qt.query_objects: 156 | query = q.query_f('0') 157 | logger.debug("Validating [%s]" % query) 158 | try: 159 | conn.query(query) 160 | conn.store_result() 161 | except _mysql.MySQLError as e: 162 | n,m = e 163 | if n in worker.uncaught_errors: 164 | failed_queries[q.query_id] = str(e) 165 | else: 166 | logger.debug("Uncaught [%d] : %s" % (n, m)) 167 | 168 | if len(failed_queries) > 0: 169 | raise WorkloadException(failed_queries) 170 | 171 | if qt.is_empty(): 172 | raise ServerException("Workload is empty. Adjust the dial to indicate how many times to run the query per-second.", ServerException.ER_JS) 173 | 174 | logger.debug("VALID WORKLOAD!") 175 | finally: 176 | conn.close() 177 | except _mysql.MySQLError as e: 178 | check_connection_settings(g_settings) 179 | raise ServerException('Unable to validate workload. Could not connect to database.', ServerException.ER_UNKNOWN) 180 | 181 | def check_connection_settings(settings): 182 | try: 183 | conn = settings.get_db_conn() 184 | except database.MySQLError as e: 185 | n,m = e 186 | if n == worker.ER_DB_DNE: 187 | raise ServerException(str(e), ServerException.ER_DBNAME) 188 | else: 189 | raise ServerException(str(e), ServerException.ER_DBCONN) 190 | 191 | try: 192 | conn.query('show tables') 193 | except database.MySQLError as e: 194 | raise ServerException(str(e), ServerException.ER_DBNAME) 195 | finally: 196 | conn.close() 197 | 198 | # Raises a ServerException with a human readable error if the workers 199 | # aren't in tip-top shape. 200 | def check_workers(): 201 | global g_workers 202 | global g_settings 203 | 204 | if not g_workers.is_alive(): 205 | check_connection_settings(g_settings) 206 | raise ServerException("Unable to initialize workers.") 207 | 208 | logger.debug("Checked workers PASSED") 209 | 210 | def reset_workers(): 211 | global g_workers 212 | global g_settings 213 | 214 | if g_workers is not None: 215 | g_workers.clear() 216 | logger.info("Creating %d workers", g_settings.workers) 217 | g_workers = worker.WorkerPool(g_settings) 218 | 219 | @app.route('/', methods=['GET']) 220 | def render_index(): 221 | global g_last_ping 222 | global g_settings 223 | 224 | if (datetime.now() - g_last_ping).seconds <= PING_TIMEOUT: 225 | return redirect(url_for('get_live_page', redirected=True)); 226 | else: 227 | g_last_ping = datetime.now(); 228 | return render_template('index.html', settings=g_settings, live=False) 229 | 230 | @app.route('/workload', methods=['POST']) 231 | def submit_workload(): 232 | global g_workers 233 | global g_workload 234 | global g_settings 235 | 236 | try: 237 | g_workload = {} 238 | 239 | for query_id, info in json.loads(request.values.get('workload')).items(): 240 | try: 241 | info['qps'] = int(info['qps']) 242 | except ValueError as e: 243 | info['qps'] = int(float(info['qps'])) 244 | g_workload[int(query_id)] = info 245 | if len(g_workload) == 0: 246 | raise ServerException("The workload table is empty. Use the workspace below to add new queries.", \ 247 | ServerException.ER_JS) 248 | 249 | g_settings = Settings(json.loads(request.values.get('settings'))) 250 | if g_workers is None \ 251 | or not g_workers.is_alive() \ 252 | or g_workers.settings_dict != g_settings.get_dict(): 253 | reset_workers() 254 | 255 | check_workers() 256 | 257 | validate_workload() 258 | 259 | if not g_workers.pause(): 260 | raise ServerException("Unknown Error- workers failed") 261 | 262 | g_workers.send_workload(g_workload) 263 | return format_response(True) 264 | except ServerException as e: 265 | return format_response(False, e) 266 | 267 | @app.route('/ping', methods=['POST']) 268 | def ping(): 269 | global g_last_ping 270 | 271 | if (datetime.now() - g_last_ping).seconds <= PING_TIMEOUT: 272 | g_last_ping = datetime.now() 273 | return 'OK' 274 | else: 275 | return 'Timeout' 276 | 277 | @app.route('/unload', methods=['PUT']) 278 | def unload(): 279 | global g_last_ping 280 | g_last_ping = VERY_LONG_AGO 281 | return '' 282 | 283 | @app.route('/pause', methods=['POST']) 284 | def pause(): 285 | global g_workers 286 | 287 | try: 288 | if g_workers is None or g_workers.paused: 289 | return format_response(False) 290 | 291 | check_workers() 292 | 293 | g_workers.pause() 294 | assert g_workers.paused 295 | return format_response(False) 296 | except ServerException as e: 297 | return format_response(False, e) 298 | 299 | stat_check_count = 0 300 | full_check_period = 10 # should work out to once per second 301 | @app.route('/stats', methods=['get']) 302 | def get_stats(): 303 | global g_workers 304 | global g_workload 305 | global stat_check_count 306 | global full_check_period 307 | 308 | try: 309 | if g_workers is None: 310 | return format_response(False) 311 | 312 | if stat_check_count % full_check_period == 0: 313 | check_workers() 314 | 315 | stat_check_count += 1 316 | 317 | ret = {} 318 | for query_id, info in g_workload.items(): 319 | ret[query_id] = max(0, g_workers.qps_array[query_id]) 320 | 321 | return format_response(True, None, stats=ret) 322 | except ServerException as e: 323 | return format_response(False, e) 324 | 325 | 326 | @app.route('/status', methods=['GET']) 327 | def get_status(): 328 | global g_workers 329 | global g_workload 330 | 331 | if g_workers is None: 332 | return format_response(running=False, error=None) 333 | 334 | try: 335 | check_workers() 336 | running = not g_workers.paused 337 | return format_response(running=running, error=None) 338 | except ServerException as e: 339 | return format_response(False, e) 340 | 341 | @app.route('/live', methods=['GET']) 342 | def get_live_page(): 343 | global g_settings 344 | redirected = request.values.get('redirected') 345 | return render_template('index.html', settings=g_settings, live=True, redirected=redirected) 346 | 347 | 348 | @app.route('/live/stats', methods=['GET']) 349 | def get_live_stats(): 350 | global g_plancaches 351 | 352 | settings = Settings(json.loads(request.values.get('settings', '{}'))) 353 | my_plancache = plancache.plancacheFactory(settings) 354 | 355 | try: 356 | return format_response(True, None, plancache=json.dumps(my_plancache.get_stats()), metrics=json.dumps(my_plancache.get_metrics())) 357 | except plancache.PlancacheBroken: 358 | # broken pipe to worker process- something's up 359 | try: 360 | check_connection_settings(settings) 361 | raise ServerException("Unable to access plancache") 362 | except ServerException as e: 363 | return format_response(False, e) 364 | 365 | def get_use_db(cmd): 366 | cmd = cmd.lstrip().rstrip() 367 | match = re.search(r'^use\s+([^\s]*)\s*;$', cmd) 368 | if match: 369 | return match.group(1) 370 | else: 371 | None 372 | 373 | @app.route('/sql', methods=['GET']) 374 | def run_sql_command(): 375 | print 'SQL COMMAND' 376 | print request.values.get('settings') 377 | 378 | try: 379 | settings = Settings(json.loads(request.values.get('settings'))) 380 | 381 | sql_command = re.sub(r'"', r'\\"', request.values.get('command')) 382 | 383 | args = settings.get_client_arguments() 384 | 385 | while True: 386 | command = 'mysql -h %(host)s --socket=%(unix_socket)s --port=%(port)d -u %(user)s --password=%(passwd)s %(db)s --table -vvv;' % args 387 | p = subprocess.Popen(command, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 388 | stdout, stderr = p.communicate(sql_command) 389 | exit_code = p.returncode 390 | if (string.count(stdout, '\n') <= 100): 391 | print stdout, stderr 392 | else: 393 | print 'Output too long to print', stderr 394 | 395 | ret = {} 396 | if exit_code == 0: 397 | output = stdout 398 | try: 399 | output = re.split(r'^--------------$', output, maxsplit=3, flags=re.MULTILINE)[2] 400 | except IndexError as e: 401 | pass 402 | output = output.split('Bye')[0] 403 | output = output.lstrip().rstrip() 404 | if output != '': 405 | output = output + "\n" 406 | db_name = get_use_db(sql_command) 407 | if db_name: 408 | output = "Database Changed" 409 | ret['db'] = db_name 410 | break 411 | else: 412 | output = stderr 413 | if re.search(r'^ERROR 1049', output) and args['db'] != '': 414 | args['db'] = '' 415 | else: 416 | if re.search(r'^ERROR 2003', output): # can't connect to mysql 417 | raise ServerException(output, ServerException.ER_DBCONN) 418 | else: 419 | break 420 | 421 | ret['output'] = output 422 | return json.dumps(ret) 423 | except ServerException as e: 424 | return format_response(False, e); 425 | 426 | @app.route('/save', methods=['GET']) 427 | def save_session(): 428 | settings = request.values.get('settings', None) 429 | if settings: 430 | save_settings = json.loads(settings) 431 | else: 432 | save_session = Settings(json.loads(settings)).get_dict() 433 | 434 | workload = request.values.get('workload', None) 435 | if workload: 436 | save_workload = json.loads(workload) 437 | else: 438 | save_workload = g_workload 439 | 440 | contents = json.dumps({'settings' : save_settings, 'workload' : save_workload}) 441 | headers = {'Content-Type' : 'application/x-download', 'Content-Disposition' : 'attachment;filename=workload.json'} 442 | print contents 443 | return make_response((contents, 200, headers)) 444 | 445 | @app.route('/kill', methods=['GET']) 446 | def kill(): 447 | global g_workers 448 | global g_plancaches 449 | 450 | if g_workers: 451 | g_workers.clear() 452 | if g_plancaches: 453 | del g_plancaches 454 | sys.exit() 455 | 456 | def main(): 457 | global g_settings 458 | 459 | options = utils.get_options() 460 | 461 | g_settings = Settings({}) 462 | g_settings.memsql_host = options.memsql_host 463 | g_settings.memsql_port = options.memsql_port 464 | g_settings.memsql_user = options.memsql_user 465 | g_settings.memsql_pass = options.memsql_pass 466 | g_settings.memsql_db = options.memsql_db 467 | g_settings.workers = options.workers 468 | 469 | app.run(debug=True, port=options.server_port, host='0.0.0.0') 470 | 471 | if __name__ == '__main__': 472 | main() 473 | -------------------------------------------------------------------------------- /stress/static/css/bootstrap-responsive.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.3 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | .clearfix { 12 | *zoom: 1; 13 | } 14 | 15 | .clearfix:before, 16 | .clearfix:after { 17 | display: table; 18 | content: ""; 19 | } 20 | 21 | .clearfix:after { 22 | clear: both; 23 | } 24 | 25 | .hide-text { 26 | font: 0/0 a; 27 | color: transparent; 28 | text-shadow: none; 29 | background-color: transparent; 30 | border: 0; 31 | } 32 | 33 | .input-block-level { 34 | display: block; 35 | width: 100%; 36 | min-height: 28px; 37 | -webkit-box-sizing: border-box; 38 | -moz-box-sizing: border-box; 39 | -ms-box-sizing: border-box; 40 | box-sizing: border-box; 41 | } 42 | 43 | .hidden { 44 | display: none; 45 | visibility: hidden; 46 | } 47 | 48 | .visible-phone { 49 | display: none !important; 50 | } 51 | 52 | .visible-tablet { 53 | display: none !important; 54 | } 55 | 56 | .hidden-desktop { 57 | display: none !important; 58 | } 59 | 60 | @media (max-width: 767px) { 61 | .visible-phone { 62 | display: inherit !important; 63 | } 64 | .hidden-phone { 65 | display: none !important; 66 | } 67 | .hidden-desktop { 68 | display: inherit !important; 69 | } 70 | .visible-desktop { 71 | display: none !important; 72 | } 73 | } 74 | 75 | @media (min-width: 768px) and (max-width: 979px) { 76 | .visible-tablet { 77 | display: inherit !important; 78 | } 79 | .hidden-tablet { 80 | display: none !important; 81 | } 82 | .hidden-desktop { 83 | display: inherit !important; 84 | } 85 | .visible-desktop { 86 | display: none !important ; 87 | } 88 | } 89 | 90 | @media (max-width: 480px) { 91 | .nav-collapse { 92 | -webkit-transform: translate3d(0, 0, 0); 93 | } 94 | .page-header h1 small { 95 | display: block; 96 | line-height: 18px; 97 | } 98 | input[type="checkbox"], 99 | input[type="radio"] { 100 | border: 1px solid #ccc; 101 | } 102 | .form-horizontal .control-group > label { 103 | float: none; 104 | width: auto; 105 | padding-top: 0; 106 | text-align: left; 107 | } 108 | .form-horizontal .controls { 109 | margin-left: 0; 110 | } 111 | .form-horizontal .control-list { 112 | padding-top: 0; 113 | } 114 | .form-horizontal .form-actions { 115 | padding-right: 10px; 116 | padding-left: 10px; 117 | } 118 | .modal { 119 | position: absolute; 120 | top: 10px; 121 | right: 10px; 122 | left: 10px; 123 | width: auto; 124 | margin: 0; 125 | } 126 | .modal.fade.in { 127 | top: auto; 128 | } 129 | .modal-header .close { 130 | padding: 10px; 131 | margin: -10px; 132 | } 133 | .carousel-caption { 134 | position: static; 135 | } 136 | } 137 | 138 | @media (max-width: 767px) { 139 | body { 140 | padding-right: 20px; 141 | padding-left: 20px; 142 | } 143 | .navbar-fixed-top, 144 | .navbar-fixed-bottom { 145 | margin-right: -20px; 146 | margin-left: -20px; 147 | } 148 | .container-fluid { 149 | padding: 0; 150 | } 151 | .dl-horizontal dt { 152 | float: none; 153 | width: auto; 154 | clear: none; 155 | text-align: left; 156 | } 157 | .dl-horizontal dd { 158 | margin-left: 0; 159 | } 160 | .container { 161 | width: auto; 162 | } 163 | .row-fluid { 164 | width: 100%; 165 | } 166 | .row, 167 | .thumbnails { 168 | margin-left: 0; 169 | } 170 | [class*="span"], 171 | .row-fluid [class*="span"] { 172 | display: block; 173 | float: none; 174 | width: auto; 175 | margin-left: 0; 176 | } 177 | .input-large, 178 | .input-xlarge, 179 | .input-xxlarge, 180 | input[class*="span"], 181 | select[class*="span"], 182 | textarea[class*="span"], 183 | .uneditable-input { 184 | display: block; 185 | width: 100%; 186 | min-height: 28px; 187 | -webkit-box-sizing: border-box; 188 | -moz-box-sizing: border-box; 189 | -ms-box-sizing: border-box; 190 | box-sizing: border-box; 191 | } 192 | .input-prepend input, 193 | .input-append input, 194 | .input-prepend input[class*="span"], 195 | .input-append input[class*="span"] { 196 | display: inline-block; 197 | width: auto; 198 | } 199 | } 200 | 201 | @media (min-width: 768px) and (max-width: 979px) { 202 | .row { 203 | margin-left: -20px; 204 | *zoom: 1; 205 | } 206 | .row:before, 207 | .row:after { 208 | display: table; 209 | content: ""; 210 | } 211 | .row:after { 212 | clear: both; 213 | } 214 | [class*="span"] { 215 | float: left; 216 | margin-left: 20px; 217 | } 218 | .container, 219 | .navbar-fixed-top .container, 220 | .navbar-fixed-bottom .container { 221 | width: 724px; 222 | } 223 | .span12 { 224 | width: 724px; 225 | } 226 | .span11 { 227 | width: 662px; 228 | } 229 | .span10 { 230 | width: 600px; 231 | } 232 | .span9 { 233 | width: 538px; 234 | } 235 | .span8 { 236 | width: 476px; 237 | } 238 | .span7 { 239 | width: 414px; 240 | } 241 | .span6 { 242 | width: 352px; 243 | } 244 | .span5 { 245 | width: 290px; 246 | } 247 | .span4 { 248 | width: 228px; 249 | } 250 | .span3 { 251 | width: 166px; 252 | } 253 | .span2 { 254 | width: 104px; 255 | } 256 | .span1 { 257 | width: 42px; 258 | } 259 | .offset12 { 260 | margin-left: 764px; 261 | } 262 | .offset11 { 263 | margin-left: 702px; 264 | } 265 | .offset10 { 266 | margin-left: 640px; 267 | } 268 | .offset9 { 269 | margin-left: 578px; 270 | } 271 | .offset8 { 272 | margin-left: 516px; 273 | } 274 | .offset7 { 275 | margin-left: 454px; 276 | } 277 | .offset6 { 278 | margin-left: 392px; 279 | } 280 | .offset5 { 281 | margin-left: 330px; 282 | } 283 | .offset4 { 284 | margin-left: 268px; 285 | } 286 | .offset3 { 287 | margin-left: 206px; 288 | } 289 | .offset2 { 290 | margin-left: 144px; 291 | } 292 | .offset1 { 293 | margin-left: 82px; 294 | } 295 | .row-fluid { 296 | width: 100%; 297 | *zoom: 1; 298 | } 299 | .row-fluid:before, 300 | .row-fluid:after { 301 | display: table; 302 | content: ""; 303 | } 304 | .row-fluid:after { 305 | clear: both; 306 | } 307 | .row-fluid [class*="span"] { 308 | display: block; 309 | float: left; 310 | width: 100%; 311 | min-height: 28px; 312 | margin-left: 2.762430939%; 313 | *margin-left: 2.709239449638298%; 314 | -webkit-box-sizing: border-box; 315 | -moz-box-sizing: border-box; 316 | -ms-box-sizing: border-box; 317 | box-sizing: border-box; 318 | } 319 | .row-fluid [class*="span"]:first-child { 320 | margin-left: 0; 321 | } 322 | .row-fluid .span12 { 323 | width: 99.999999993%; 324 | *width: 99.9468085036383%; 325 | } 326 | .row-fluid .span11 { 327 | width: 91.436464082%; 328 | *width: 91.38327259263829%; 329 | } 330 | .row-fluid .span10 { 331 | width: 82.87292817100001%; 332 | *width: 82.8197366816383%; 333 | } 334 | .row-fluid .span9 { 335 | width: 74.30939226%; 336 | *width: 74.25620077063829%; 337 | } 338 | .row-fluid .span8 { 339 | width: 65.74585634900001%; 340 | *width: 65.6926648596383%; 341 | } 342 | .row-fluid .span7 { 343 | width: 57.182320438000005%; 344 | *width: 57.129128948638304%; 345 | } 346 | .row-fluid .span6 { 347 | width: 48.618784527%; 348 | *width: 48.5655930376383%; 349 | } 350 | .row-fluid .span5 { 351 | width: 40.055248616%; 352 | *width: 40.0020571266383%; 353 | } 354 | .row-fluid .span4 { 355 | width: 31.491712705%; 356 | *width: 31.4385212156383%; 357 | } 358 | .row-fluid .span3 { 359 | width: 22.928176794%; 360 | *width: 22.874985304638297%; 361 | } 362 | .row-fluid .span2 { 363 | width: 14.364640883%; 364 | *width: 14.311449393638298%; 365 | } 366 | .row-fluid .span1 { 367 | width: 5.801104972%; 368 | *width: 5.747913482638298%; 369 | } 370 | input, 371 | textarea, 372 | .uneditable-input { 373 | margin-left: 0; 374 | } 375 | input.span12, 376 | textarea.span12, 377 | .uneditable-input.span12 { 378 | width: 714px; 379 | } 380 | input.span11, 381 | textarea.span11, 382 | .uneditable-input.span11 { 383 | width: 652px; 384 | } 385 | input.span10, 386 | textarea.span10, 387 | .uneditable-input.span10 { 388 | width: 590px; 389 | } 390 | input.span9, 391 | textarea.span9, 392 | .uneditable-input.span9 { 393 | width: 528px; 394 | } 395 | input.span8, 396 | textarea.span8, 397 | .uneditable-input.span8 { 398 | width: 466px; 399 | } 400 | input.span7, 401 | textarea.span7, 402 | .uneditable-input.span7 { 403 | width: 404px; 404 | } 405 | input.span6, 406 | textarea.span6, 407 | .uneditable-input.span6 { 408 | width: 342px; 409 | } 410 | input.span5, 411 | textarea.span5, 412 | .uneditable-input.span5 { 413 | width: 280px; 414 | } 415 | input.span4, 416 | textarea.span4, 417 | .uneditable-input.span4 { 418 | width: 218px; 419 | } 420 | input.span3, 421 | textarea.span3, 422 | .uneditable-input.span3 { 423 | width: 156px; 424 | } 425 | input.span2, 426 | textarea.span2, 427 | .uneditable-input.span2 { 428 | width: 94px; 429 | } 430 | input.span1, 431 | textarea.span1, 432 | .uneditable-input.span1 { 433 | width: 32px; 434 | } 435 | } 436 | 437 | @media (min-width: 1200px) { 438 | .row { 439 | margin-left: -30px; 440 | *zoom: 1; 441 | } 442 | .row:before, 443 | .row:after { 444 | display: table; 445 | content: ""; 446 | } 447 | .row:after { 448 | clear: both; 449 | } 450 | [class*="span"] { 451 | float: left; 452 | margin-left: 30px; 453 | } 454 | .container, 455 | .navbar-fixed-top .container, 456 | .navbar-fixed-bottom .container { 457 | width: 1170px; 458 | } 459 | .span12 { 460 | width: 1170px; 461 | } 462 | .span11 { 463 | width: 1070px; 464 | } 465 | .span10 { 466 | width: 970px; 467 | } 468 | .span9 { 469 | width: 870px; 470 | } 471 | .span8 { 472 | width: 770px; 473 | } 474 | .span7 { 475 | width: 670px; 476 | } 477 | .span6 { 478 | width: 570px; 479 | } 480 | .span5 { 481 | width: 470px; 482 | } 483 | .span4 { 484 | width: 370px; 485 | } 486 | .span3 { 487 | width: 270px; 488 | } 489 | .span2 { 490 | width: 170px; 491 | } 492 | .span1 { 493 | width: 70px; 494 | } 495 | .offset12 { 496 | margin-left: 1230px; 497 | } 498 | .offset11 { 499 | margin-left: 1130px; 500 | } 501 | .offset10 { 502 | margin-left: 1030px; 503 | } 504 | .offset9 { 505 | margin-left: 930px; 506 | } 507 | .offset8 { 508 | margin-left: 830px; 509 | } 510 | .offset7 { 511 | margin-left: 730px; 512 | } 513 | .offset6 { 514 | margin-left: 630px; 515 | } 516 | .offset5 { 517 | margin-left: 530px; 518 | } 519 | .offset4 { 520 | margin-left: 430px; 521 | } 522 | .offset3 { 523 | margin-left: 330px; 524 | } 525 | .offset2 { 526 | margin-left: 230px; 527 | } 528 | .offset1 { 529 | margin-left: 130px; 530 | } 531 | .row-fluid { 532 | width: 100%; 533 | *zoom: 1; 534 | } 535 | .row-fluid:before, 536 | .row-fluid:after { 537 | display: table; 538 | content: ""; 539 | } 540 | .row-fluid:after { 541 | clear: both; 542 | } 543 | .row-fluid [class*="span"] { 544 | display: block; 545 | float: left; 546 | width: 100%; 547 | min-height: 28px; 548 | margin-left: 2.564102564%; 549 | *margin-left: 2.510911074638298%; 550 | -webkit-box-sizing: border-box; 551 | -moz-box-sizing: border-box; 552 | -ms-box-sizing: border-box; 553 | box-sizing: border-box; 554 | } 555 | .row-fluid [class*="span"]:first-child { 556 | margin-left: 0; 557 | } 558 | .row-fluid .span12 { 559 | width: 100%; 560 | *width: 99.94680851063829%; 561 | } 562 | .row-fluid .span11 { 563 | width: 91.45299145300001%; 564 | *width: 91.3997999636383%; 565 | } 566 | .row-fluid .span10 { 567 | width: 82.905982906%; 568 | *width: 82.8527914166383%; 569 | } 570 | .row-fluid .span9 { 571 | width: 74.358974359%; 572 | *width: 74.30578286963829%; 573 | } 574 | .row-fluid .span8 { 575 | width: 65.81196581200001%; 576 | *width: 65.7587743226383%; 577 | } 578 | .row-fluid .span7 { 579 | width: 57.264957265%; 580 | *width: 57.2117657756383%; 581 | } 582 | .row-fluid .span6 { 583 | width: 48.717948718%; 584 | *width: 48.6647572286383%; 585 | } 586 | .row-fluid .span5 { 587 | width: 40.170940171000005%; 588 | *width: 40.117748681638304%; 589 | } 590 | .row-fluid .span4 { 591 | width: 31.623931624%; 592 | *width: 31.5707401346383%; 593 | } 594 | .row-fluid .span3 { 595 | width: 23.076923077%; 596 | *width: 23.0237315876383%; 597 | } 598 | .row-fluid .span2 { 599 | width: 14.529914530000001%; 600 | *width: 14.4767230406383%; 601 | } 602 | .row-fluid .span1 { 603 | width: 5.982905983%; 604 | *width: 5.929714493638298%; 605 | } 606 | input, 607 | textarea, 608 | .uneditable-input { 609 | margin-left: 0; 610 | } 611 | input.span12, 612 | textarea.span12, 613 | .uneditable-input.span12 { 614 | width: 1160px; 615 | } 616 | input.span11, 617 | textarea.span11, 618 | .uneditable-input.span11 { 619 | width: 1060px; 620 | } 621 | input.span10, 622 | textarea.span10, 623 | .uneditable-input.span10 { 624 | width: 960px; 625 | } 626 | input.span9, 627 | textarea.span9, 628 | .uneditable-input.span9 { 629 | width: 860px; 630 | } 631 | input.span8, 632 | textarea.span8, 633 | .uneditable-input.span8 { 634 | width: 760px; 635 | } 636 | input.span7, 637 | textarea.span7, 638 | .uneditable-input.span7 { 639 | width: 660px; 640 | } 641 | input.span6, 642 | textarea.span6, 643 | .uneditable-input.span6 { 644 | width: 560px; 645 | } 646 | input.span5, 647 | textarea.span5, 648 | .uneditable-input.span5 { 649 | width: 460px; 650 | } 651 | input.span4, 652 | textarea.span4, 653 | .uneditable-input.span4 { 654 | width: 360px; 655 | } 656 | input.span3, 657 | textarea.span3, 658 | .uneditable-input.span3 { 659 | width: 260px; 660 | } 661 | input.span2, 662 | textarea.span2, 663 | .uneditable-input.span2 { 664 | width: 160px; 665 | } 666 | input.span1, 667 | textarea.span1, 668 | .uneditable-input.span1 { 669 | width: 60px; 670 | } 671 | .thumbnails { 672 | margin-left: -30px; 673 | } 674 | .thumbnails > li { 675 | margin-left: 30px; 676 | } 677 | .row-fluid .thumbnails { 678 | margin-left: 0; 679 | } 680 | } 681 | 682 | @media (max-width: 979px) { 683 | body { 684 | padding-top: 0; 685 | } 686 | .navbar-fixed-top { 687 | position: static; 688 | margin-bottom: 18px; 689 | } 690 | .navbar-fixed-top .navbar-inner { 691 | padding: 5px; 692 | } 693 | .navbar .container { 694 | width: auto; 695 | padding: 0; 696 | } 697 | .navbar .brand { 698 | padding-right: 10px; 699 | padding-left: 10px; 700 | margin: 0 0 0 -5px; 701 | } 702 | .nav-collapse { 703 | clear: both; 704 | } 705 | .nav-collapse .nav { 706 | float: none; 707 | margin: 0 0 9px; 708 | } 709 | .nav-collapse .nav > li { 710 | float: none; 711 | } 712 | .nav-collapse .nav > li > a { 713 | margin-bottom: 2px; 714 | } 715 | .nav-collapse .nav > .divider-vertical { 716 | display: none; 717 | } 718 | .nav-collapse .nav .nav-header { 719 | color: #999999; 720 | text-shadow: none; 721 | } 722 | .nav-collapse .nav > li > a, 723 | .nav-collapse .dropdown-menu a { 724 | padding: 6px 15px; 725 | font-weight: bold; 726 | color: #999999; 727 | -webkit-border-radius: 3px; 728 | -moz-border-radius: 3px; 729 | border-radius: 3px; 730 | } 731 | .nav-collapse .btn { 732 | padding: 4px 10px 4px; 733 | font-weight: normal; 734 | -webkit-border-radius: 4px; 735 | -moz-border-radius: 4px; 736 | border-radius: 4px; 737 | } 738 | .nav-collapse .dropdown-menu li + li a { 739 | margin-bottom: 2px; 740 | } 741 | .nav-collapse .nav > li > a:hover, 742 | .nav-collapse .dropdown-menu a:hover { 743 | background-color: #222222; 744 | } 745 | .nav-collapse.in .btn-group { 746 | padding: 0; 747 | margin-top: 5px; 748 | } 749 | .nav-collapse .dropdown-menu { 750 | position: static; 751 | top: auto; 752 | left: auto; 753 | display: block; 754 | float: none; 755 | max-width: none; 756 | padding: 0; 757 | margin: 0 15px; 758 | background-color: transparent; 759 | border: none; 760 | -webkit-border-radius: 0; 761 | -moz-border-radius: 0; 762 | border-radius: 0; 763 | -webkit-box-shadow: none; 764 | -moz-box-shadow: none; 765 | box-shadow: none; 766 | } 767 | .nav-collapse .dropdown-menu:before, 768 | .nav-collapse .dropdown-menu:after { 769 | display: none; 770 | } 771 | .nav-collapse .dropdown-menu .divider { 772 | display: none; 773 | } 774 | .nav-collapse .navbar-form, 775 | .nav-collapse .navbar-search { 776 | float: none; 777 | padding: 9px 15px; 778 | margin: 9px 0; 779 | border-top: 1px solid #222222; 780 | border-bottom: 1px solid #222222; 781 | -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 782 | -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 783 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); 784 | } 785 | .navbar .nav-collapse .nav.pull-right { 786 | float: none; 787 | margin-left: 0; 788 | } 789 | .nav-collapse, 790 | .nav-collapse.collapse { 791 | height: 0; 792 | overflow: hidden; 793 | } 794 | .navbar .btn-navbar { 795 | display: block; 796 | } 797 | .navbar-static .navbar-inner { 798 | padding-right: 10px; 799 | padding-left: 10px; 800 | } 801 | } 802 | 803 | @media (min-width: 980px) { 804 | .nav-collapse.collapse { 805 | height: auto !important; 806 | overflow: visible !important; 807 | } 808 | } 809 | -------------------------------------------------------------------------------- /stress/static/js/jquery.knob-1.1.1.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Knob - jQuery Plugin 3 | * Downward compatible, touchable dial 4 | * 5 | * Version: 1.1.2 (22/05/2012) 6 | * Requires: jQuery v1.7+ 7 | * 8 | * Copyright (c) 2011 Anthony Terrien 9 | * Under MIT and GPL licenses: 10 | * http://www.opensource.org/licenses/mit-license.php 11 | * http://www.gnu.org/licenses/gpl.html 12 | * 13 | * Thanks to vor, eskimoblood, spiffistan 14 | */ 15 | $(function () { 16 | 17 | // Dial logic 18 | var Dial = function (c, opt) { 19 | 20 | var v = null 21 | ,ctx = c[0].getContext("2d") 22 | ,PI2 = 2 * Math.PI 23 | ,mx ,my ,x ,y 24 | ,self = this; 25 | 26 | this.first = true; 27 | this.onChange = function () {}; 28 | this.onCancel = function () {}; 29 | this.onRelease = function () {}; 30 | 31 | this.val = function (nv) { 32 | if (null != nv) { 33 | opt.stopper && (nv = Math.max(Math.min(nv, opt.max), opt.min)); 34 | v = nv; 35 | this.onChange(nv); 36 | this.draw(nv); 37 | } else { 38 | var b, a; 39 | b = a = Math.atan2(mx - x, -(my - y - opt.width / 2)) - opt.angleOffset; 40 | (a < 0) && (b = a + PI2); 41 | nv = Math.round(b * (opt.max - opt.min) / PI2) + opt.min; 42 | return (nv > opt.max) ? opt.max : nv; 43 | } 44 | }; 45 | 46 | this.change = function (nv) { 47 | opt.stopper && (nv = Math.max(Math.min(nv, opt.max), opt.min)); 48 | this.onChange(nv); 49 | this.draw(nv); 50 | }; 51 | 52 | this.angle = function (nv) { 53 | return (nv - opt.min) * PI2 / (opt.max - opt.min); 54 | }; 55 | 56 | this.draw = function (nv) { 57 | 58 | var a = this.angle(nv) // Angle 59 | ,sa = 1.5 * Math.PI + opt.angleOffset // Previous start angle 60 | ,sat = sa // Start angle 61 | ,ea = sa + this.angle(v) // Previous end angle 62 | ,eat = sat + a // End angle 63 | ,r = opt.width / 2 // Radius 64 | ,lw = r * opt.thickness // Line width 65 | ,cgcolor = Dial.getCgColor(opt.cgColor) 66 | ,tick 67 | ; 68 | 69 | ctx.clearRect(0, 0, opt.width, opt.width); 70 | ctx.lineWidth = lw; 71 | 72 | // Hook draw 73 | if (opt.draw(a, v, opt, ctx)) { return; } 74 | 75 | for (tick = 0; tick < opt.ticks; tick++) { 76 | 77 | ctx.beginPath(); 78 | 79 | if (a > (((2 * Math.PI) / opt.ticks) * tick) && opt.tickColorizeValues) { 80 | ctx.strokeStyle = opt.fgColor; 81 | } else { 82 | ctx.strokeStyle = opt.tickColor; 83 | } 84 | 85 | var tick_sa = (((2 * Math.PI) / opt.ticks) * tick) - (0.5 * Math.PI); 86 | ctx.arc( r, r, r-lw-opt.tickLength, tick_sa, tick_sa+opt.tickWidth , false); 87 | ctx.stroke(); 88 | } 89 | 90 | opt.cursor 91 | && (sa = ea - 0.3) 92 | && (ea = ea + 0.3) 93 | && (sat = eat - 0.3) 94 | && (eat = eat + 0.3); 95 | 96 | switch (opt.skin) { 97 | 98 | case 'default' : 99 | 100 | ctx.beginPath(); 101 | ctx.strokeStyle = opt.bgColor; 102 | ctx.arc(r, r, r - lw / 2, 0, PI2, true); 103 | ctx.stroke(); 104 | 105 | if (opt.displayPrevious && !opt.readOnly) { 106 | ctx.beginPath(); 107 | ctx.strokeStyle = (v == nv) ? opt.fgColor : cgcolor; 108 | ctx.arc(r, r, r - lw / 2, sa, ea, false); 109 | ctx.stroke(); 110 | } 111 | if (opt.readOnlyStart && this.first && sa != ea) { 112 | this.first_strokeStyle = cgcolor; 113 | this.first_r = r; 114 | this.first_lw = lw; 115 | this.first_sa = sa; 116 | this.first_ea = ea; 117 | this.first = false; 118 | } 119 | if (opt.readOnlyStart && opt.readOnly && opt.displayPrevious) 120 | { 121 | ctx.beginPath(); 122 | ctx.strokeStyle = this.first_strokeStyle; 123 | ctx.arc(this.first_r, this.first_r, this.first_r - this.first_lw / 2, this.first_sa, this.first_ea, false); 124 | ctx.stroke(); 125 | } 126 | 127 | 128 | ctx.beginPath(); 129 | ctx.strokeStyle = opt.fgColor; 130 | ctx.arc(r, r, r - lw / 2, sat, eat, false); 131 | ctx.stroke(); 132 | 133 | break; 134 | 135 | case 'tron' : 136 | 137 | if (opt.displayPrevious) { 138 | ctx.beginPath(); 139 | ctx.strokeStyle = (v == nv) ? opt.fgColor : cgcolor; 140 | ctx.arc( r, r, r - lw, sa, ea, false); 141 | ctx.stroke(); 142 | } 143 | 144 | ctx.beginPath(); 145 | ctx.strokeStyle = opt.fgColor; 146 | ctx.arc( r, r, r - lw, sat, eat, false); 147 | ctx.stroke(); 148 | 149 | ctx.lineWidth = 2; 150 | ctx.beginPath(); 151 | ctx.strokeStyle = opt.fgColor; 152 | ctx.arc( r, r, r - lw + 1 + lw * 2 / 3, 0, 2 * Math.PI, false); 153 | ctx.stroke(); 154 | 155 | break; 156 | } 157 | }; 158 | 159 | this.capture = function (e) { 160 | switch (e.type) { 161 | case 'mousemove' : 162 | case 'mousedown' : 163 | mx = e.pageX; 164 | my = e.pageY; 165 | break; 166 | case 'touchmove' : 167 | case 'touchstart' : 168 | mx = e.originalEvent.touches[0].pageX; 169 | my = e.originalEvent.touches[0].pageY; 170 | break; 171 | } 172 | this.change( this.val() ); 173 | }; 174 | 175 | this.cancel = function () { 176 | self.val(v); 177 | self.onCancel(); 178 | }; 179 | 180 | this.startDrag = function (e) { 181 | 182 | var p = c.offset() 183 | ,$doc = $(document); 184 | 185 | x = p.left + (opt.width / 2); 186 | y = p.top; 187 | 188 | this.capture(e); 189 | 190 | // Listen mouse and touch events 191 | $doc.bind( 192 | "mousemove.dial touchmove.dial" 193 | ,function (e) { 194 | self.capture(e); 195 | } 196 | ) 197 | .bind( 198 | // Escape 199 | "keyup.dial" 200 | ,function (e) { 201 | if(e.keyCode === 27) { 202 | $doc.unbind("mouseup.dial mousemove.dial keyup.dial"); 203 | self.cancel(); 204 | } 205 | } 206 | ) 207 | .bind( 208 | "mouseup.dial touchend.dial" 209 | ,function (e) { 210 | $doc.unbind('mousemove.dial touchmove.dial mouseup.dial touchend.dial keyup.dial'); 211 | self.val(self.val()); 212 | self.onRelease(v); 213 | } 214 | ); 215 | }; 216 | }; 217 | 218 | // Dial static func 219 | Dial.getCgColor = function (h) { 220 | h = h.substring(1,7); 221 | var rgb = [parseInt(h.substring(0,2),16) 222 | ,parseInt(h.substring(2,4),16) 223 | ,parseInt(h.substring(4,6),16)]; 224 | return "rgba("+rgb[0]+","+rgb[1]+","+rgb[2]+",.5)"; 225 | }; 226 | 227 | // jQuery plugin 228 | $.fn.knob = $.fn.dial = function (gopt) { 229 | 230 | return this.each( 231 | 232 | function () { 233 | 234 | var $this = $(this), opt; 235 | 236 | if ($this.data('dialed')) { return $this; } 237 | $this.data('dialed', true); 238 | 239 | opt = $.extend( 240 | { 241 | // Config 242 | 'min' : $this.data('min') || 0 243 | ,'max' : $this.data('max') || 100 244 | ,'stopper' : true 245 | ,'readOnly' : $this.data('readonly') 246 | , 'readOnlyStart' : $this.data('readonlystart') 247 | 248 | // UI 249 | ,'cursor' : $this.data('cursor') 250 | ,'thickness' : $this.data('thickness') || 0.35 251 | ,'width' : $this.data('width') || 200 252 | ,'displayInput' : $this.data('displayinput') == null || $this.data('displayinput') 253 | ,'displayPrevious' : $this.data('displayprevious') 254 | ,'fgColor' : $this.data('fgcolor') || '#87CEEB' 255 | ,'cgColor' : $this.data('cgcolor') || $this.data('fgcolor') || '#87CEEB' 256 | ,'bgColor' : $this.data('bgcolor') || '#EEEEEE' 257 | ,'tickColor' : $this.data('tickColor') || $this.data('fgcolor') || '#DDDDDD' 258 | ,'ticks' : $this.data('ticks') || 0 259 | ,'tickLength' : $this.data('tickLength') || 0 260 | ,'tickWidth' : $this.data('tickWidth') || 0.02 261 | ,'tickColorizeValues' : $this.data('tickColorizeValues') || true 262 | ,'skin' : $this.data('skin') || 'default' 263 | ,'angleOffset': degreeToRadians($this.data('angleoffset')) 264 | 265 | // Hooks 266 | ,'draw' : 267 | /** 268 | * @param int a angle 269 | * @param int v current value 270 | * @param array opt plugin options 271 | * @param context ctx Canvas context 2d 272 | * @return bool true:bypass default draw methode 273 | */ 274 | function (a, v, opt, ctx) {} 275 | ,'change' : 276 | /** 277 | * @param int v Current value 278 | */ 279 | function (v) {} 280 | ,'release' : 281 | /** 282 | * @param int v Current value 283 | * @param jQuery ipt Input 284 | */ 285 | function (v, ipt) {} 286 | } 287 | ,gopt 288 | ); 289 | 290 | var c = $('') 291 | ,wd = $('
') 292 | ,k 293 | ,vl = $this.val() 294 | ,initStyle = function () { 295 | opt.displayInput 296 | && $this.css({ 297 | 'width' : opt.width / 2 + 'px' 298 | ,'position' : 'absolute' 299 | ,'margin-top' : (opt.width * 5 / 14) + 'px' 300 | ,'margin-left' : '-' + (opt.width * 3 / 4) + 'px' 301 | ,'font-size' : (opt.width / 4) + 'px' 302 | ,'border' : 'none' 303 | ,'background' : 'none' 304 | ,'font-family' : 'Arial' 305 | ,'font-weight' : 'bold' 306 | ,'text-align' : 'center' 307 | ,'color' : opt.fgColor 308 | ,'padding' : '0px' 309 | ,'-webkit-appearance': 'none' 310 | }) 311 | || $this.css({ 312 | 'width' : '0px' 313 | ,'visibility' : 'hidden' 314 | }); 315 | }; 316 | 317 | // Canvas insert 318 | $this.wrap(wd).before(c); 319 | 320 | initStyle(); 321 | 322 | // Invoke dial logic 323 | k = new Dial(c, opt); 324 | vl || (vl = opt.min); 325 | $this.val(vl); 326 | k.val(vl); 327 | 328 | k.onRelease = function (v) { 329 | opt.release(v, $this); 330 | }; 331 | k.onChange = function (v) { 332 | $this.val(v); 333 | opt.change(v); 334 | }; 335 | 336 | // bind change on input 337 | $this.bind( 338 | 'change' 339 | ,function (e) { 340 | k.val($this.val()); 341 | } 342 | ); 343 | 344 | if (!opt.readOnly) { 345 | 346 | // canvas 347 | c.bind( 348 | "mousedown touchstart" 349 | ,function (e) { 350 | e.preventDefault(); 351 | k.startDrag(e); 352 | } 353 | ) 354 | .bind( 355 | "mousewheel DOMMouseScroll" 356 | ,mw = function (e) { 357 | e.preventDefault(); 358 | var ori = e.originalEvent 359 | ,deltaX = ori.detail || ori.wheelDeltaX 360 | ,deltaY = ori.detail || ori.wheelDeltaY 361 | ,val = parseInt($this.val()) + (deltaX>0 || deltaY>0 ? 1 : deltaX<0 || deltaY<0 ? -1 : 0); 362 | k.val(val); 363 | } 364 | ); 365 | 366 | // input 367 | var kval, val, to, m = 1, kv = {37:-1, 38:1, 39:1, 40:-1}; 368 | $this 369 | .bind( 370 | "configure" 371 | ,function (e, aconf) { 372 | var kconf; 373 | for (kconf in aconf) { opt[kconf] = aconf[kconf]; } 374 | initStyle(); 375 | k.val($this.val()); 376 | } 377 | ) 378 | .bind( 379 | "keydown" 380 | ,function (e) { 381 | var kc = e.keyCode; 382 | if (kc >= 96 && kc <= 105) kc -= 48; //numpad 383 | kval = parseInt(String.fromCharCode(kc)); 384 | 385 | if (isNaN(kval)) { 386 | 387 | (kc !== 13) // enter 388 | && (kc !== 8) // bs 389 | && (kc !== 9) // tab 390 | && (kc !== 189) // - 391 | && e.preventDefault(); 392 | 393 | // arrows 394 | if ($.inArray(kc,[37,38,39,40]) > -1) { 395 | k.change(parseInt($this.val()) + kv[kc] * m); 396 | 397 | // long time keydown speed-up 398 | to = window.setTimeout( 399 | function () { m < 20 && m++; } 400 | ,50 401 | ); 402 | 403 | e.preventDefault(); 404 | } 405 | } 406 | } 407 | ) 408 | .bind( 409 | "keyup" 410 | ,function(e) { 411 | if (isNaN(kval)) { 412 | if (to) { 413 | window.clearTimeout(to); 414 | to = null; 415 | m = 1; 416 | k.val($this.val()); 417 | k.onRelease($this.val(), $this); 418 | } else { 419 | // enter 420 | (e.keyCode === 13) 421 | && k.onRelease($this.val(), $this); 422 | } 423 | } else { 424 | // kval postcond 425 | ($this.val() > opt.max && $this.val(opt.max)) 426 | || ($this.val() < opt.min && $this.val(opt.min)); 427 | } 428 | 429 | } 430 | ) 431 | .bind( 432 | "mousewheel DOMMouseScroll" 433 | ,mw 434 | ); 435 | } else { 436 | $this.attr('readonly', 'readonly'); 437 | } 438 | } 439 | ).parent(); 440 | }; 441 | 442 | function degreeToRadians (angle) { 443 | return $.isNumeric(angle) ? angle * Math.PI / 180 : 0; 444 | } 445 | }); 446 | --------------------------------------------------------------------------------