├── .gitignore ├── LICENSE ├── README.md ├── assets ├── index.html ├── loader.gif ├── main.css ├── panic.png ├── skeleton.css ├── warning.png └── websocket.js ├── benchmarks.go ├── benchtool.go ├── bindata_assetfs.go ├── cmd.go ├── cmd_test.go ├── demo └── demo.png ├── filter.go ├── filter_test.go ├── gopath.go ├── highcharts.go ├── info.go ├── main.go ├── path.go ├── path_test.go ├── vcs.go ├── vcs_git.go ├── vcs_git_test.go ├── vcs_hg.go ├── vcs_hg_test.go ├── web.go ├── websocket.go └── workspace.go /.gitignore: -------------------------------------------------------------------------------- 1 | gobenchui 2 | .*.swp 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # GoBenchUI 3 | 4 | [![Build Status](https://drone.io/github.com/divan/gobenchui/status.png)](https://drone.io/github.com/divan/gobenchui/latest) 5 | 6 | UI for overview of your package benchmarks progress. 7 | 8 | ## Intro 9 | 10 | GoBenchUI does one simple thing - it copies your package into temporary directory, checks out every commit in source control history and run benchmarks for this version. It presents results on [highcharts.js](http://www.highcharts.com) based web UI in realtime. 11 | 12 | gobenchui demo image 13 | 14 | ## Features 15 | 16 | * nice web based UI with animation, tooltips, icons and realtime status update 17 | * separate time and memory charts 18 | * export chart to PNG, JPEG, PDF or SVG formats 19 | * supports Git and Mercurial version control 20 | * supports projects that use [gb](http://getgb.io) or GO15VENDOREXPERIMENT vendoring 21 | * advanced commits filtering 22 | * supports regexps for benchmarks 23 | * handles build errors and panics 24 | 25 | ## Installation 26 | 27 | Just go get it: 28 | 29 | go get -u github.com/divan/gobenchui 30 | 31 | ## Usage 32 | 33 | To run benchmarks, simply specify package name: 34 | 35 | gobenchui -last 10 github.com/jackpal/bencode-go 36 | 37 | or, if you're inside this directory, use `.`: 38 | 39 | cd $GOPATH/github.com/jackpal/bencode-go 40 | gobenchui -last 10 . 41 | 42 | Browser will pops up. If not, follow instructions printed to console. 43 | 44 | ## Filtering commits 45 | 46 | #### Basic filtering 47 | 48 | By default, gobenchui will run benchmarks over all commits in repository. You may want to limit commits amount to last N commits only. Use `-last` option: 49 | 50 | gobenchui -last 20 . 51 | 52 | If the number of commits is huge, but you want to get overview for complete project history, you may use `-max` option. It tries to divide all commits to N equal blocks, spread it as equally as possible and guarantee that you'll get overview for exactly N commits: 53 | 54 | gobenchui -max 15 . 55 | 56 | You also may use `-last` and `-max` in conjunction, to, say, get maximum 10 commits overview from last 100: 57 | 58 | gobenchui -max 10 -last 100 . 59 | 60 | #### VCS specific filtering 61 | 62 | If you need more powerful commits filtering, you can pass arbitrary arguments to you VCS command with `-vcsArgs`. Say, for `git`, you may specify: 63 | 64 | gobenchui -vcsArgs "--since=12.hours" . 65 | 66 | to get commits from the last 12 hours. Or: 67 | 68 | gobenchui -vcsArgs "--author Ivan --since 2.weeks --grep bench" . 69 | 70 | to get all commits by author 'Ivan' for the last 2 weeks that has word "bench" in commit message. Or: 71 | 72 | gobenchui -vcsArgs "--no-walk --tag" . 73 | 74 | to get only commits where tag was added. 75 | 76 | In other words, it's really powerful way to select only needed commits. See this [git book chapter](https://git-scm.com/book/en/v2/Git-Basics-Viewing-the-Commit-History) for more details. 77 | 78 | Note, that gobenchui will filter out args that may modify output (like `--pretty` or `--graph`), because fixed formatting is used for parsing output. 79 | 80 | ## Benchmark options 81 | 82 | In the same manner you may pass additional options to benchmarking tool. Typically you only need to specify regexp for benchmark functions: 83 | 84 | gobenchui -bench Strconv$ . 85 | 86 | It uses the same regexp rules as `go test` tool. You may also add additional flags like `-short`. 87 | 88 | ## Vendoring support 89 | 90 | `gobenchui` supports gb and GO15VENDOREXPERIMENT out of the box. It can be extended to support more vendoring solutions, as it has proper interface for that. 91 | 92 | It tries to detect right tool on each commit, so if you introduced vendoring recently, older benchmarks would work also (just make sure, needed packages still in your GOPATH before running benchmarks). 93 | 94 | I didn't test heavily that part, so there may be some bugs or corner cases I'm not aware of. 95 | 96 | ## Known issues 97 | 98 | * in case where latest commits has broken test, they will not appear in chart 99 | * may be issues with internal/ subpackages 100 | * chart icons for errors aren't exported correctly 101 | 102 | ## Contribute 103 | 104 | My frontend JS code sucks, just because, so if you want to design and implement new better web UI - you're more than welcome. 105 | 106 | Make sure to run `go generate` to regenerate assets. Or use GOBENCHUI_DEV env variable to read assets from filesystem. 107 | 108 | ## Afterwords 109 | 110 | Hopefully, this tool will bring more incentive to write benchmarks. 111 | 112 | ## License 113 | 114 | This program is under [WTFPL license](http://www.wtfpl.net) 115 | -------------------------------------------------------------------------------- /assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 107 | 108 | 109 | 110 |
111 |

Go Benchmark UI

112 |
113 |
114 | 119 | 125 | 130 |
131 |
132 |
133 |
134 |
135 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /assets/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/gobenchui/a6b1cf870779eec5def3794b05b1f7c78e0d412a/assets/loader.gif -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | } 4 | 5 | h1 { 6 | font-family: 'Helvetica Neue', 'Open Sans', sans-serif; 7 | font-size: 50px; 8 | font-weight: 300; 9 | line-height: 32px; 10 | margin: 20px 0 20px; 11 | text-align: center; 12 | } 13 | 14 | h2 { 15 | color: #111; 16 | font-family: 'Helvetica Neue', 'Open Sans', sans-serif; 17 | font-size: 28px; 18 | font-weight: 200; 19 | line-height: 22px; 20 | margin: 0 0 10px; 21 | } 22 | 23 | h3 { 24 | color: #111; 25 | font-family: 'Helvetica Neue', 'Open Sans', sans-serif; 26 | font-size: 20px; 27 | font-weight: 100; 28 | line-height: 20px; 29 | margin: 0 0 5px; 30 | } 31 | 32 | h5 { 33 | font-family: 'Helvetica Neue', 'Open Sans', sans-serif; 34 | font-size: 14px; 35 | font-weight: 300; 36 | line-height: 14px; 37 | margin: 0 0 14px; 38 | } 39 | 40 | .header { 41 | width: 100%; 42 | padding: 2px 0px; 43 | margin-bottom: 15px; 44 | -webkit-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 45 | -moz-box-shadow: 0px 3px 5px rgba(100, 100, 100, 0.49); 46 | box-shadow: 0px 3px 2px rgba(100, 100, 100, 0.49); 47 | } 48 | 49 | .footer { 50 | margin-top: 10px; 51 | padding: 5px; 52 | text-align: right; 53 | font-size: 12px; 54 | color: #0f0f0f; 55 | } 56 | 57 | .bordered { 58 | border: 1px lightgrey solid; 59 | } 60 | -------------------------------------------------------------------------------- /assets/panic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/gobenchui/a6b1cf870779eec5def3794b05b1f7c78e0d412a/assets/panic.png -------------------------------------------------------------------------------- /assets/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /assets/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/gobenchui/a6b1cf870779eec5def3794b05b1f7c78e0d412a/assets/warning.png -------------------------------------------------------------------------------- /assets/websocket.js: -------------------------------------------------------------------------------- 1 | var sock = new WebSocket(wsuri); 2 | 3 | sock.onopen = function() { console.log("connected to " + wsuri); } 4 | sock.onclose = function(e) { console.log("connection closed (" + e.code + ")"); } 5 | sock.onmessage = function(e) { 6 | var chart_time = $('#time_benchmark').highcharts(); 7 | var chart_mem = $('#mem_benchmark').highcharts(); 8 | 9 | msg = JSON.parse(e.data); 10 | 11 | if (msg.type === 'status') { 12 | date = moment(msg.commit.date).format('YYYY-MM-DD HH:mm:ss'); 13 | name = xvalue(msg.commit); 14 | document.getElementById('status').innerHTML = msg.status; 15 | 16 | // hide loader if not 'in progress' 17 | if (msg.status !== 'In progress') { 18 | document.getElementById('status_image').style.visibility = "hidden"; 19 | }; 20 | 21 | // add markers on error 22 | if (msg.error !== undefined) { 23 | // TODO: these icons somehow doesn't work with exported highcharts.js png/jpg 24 | // find out why (already spent 2 hours) and replace with working ones. 25 | marker = 'url(/static/warning.png)'; 26 | if (msg.error.Type === 'panic') { 27 | marker = 'url(/static/panic.png)'; 28 | } 29 | 30 | item = { 31 | name: name, 32 | y: 0, 33 | marker: { symbol: marker } 34 | }; 35 | 36 | $.each(chart_time.series, function(index, serie) { 37 | serie.addPoint(item, true, false); 38 | }); 39 | $.each(chart_mem.series, function(index, serie) { 40 | serie.addPoint(item, true, false); 41 | }); 42 | }; 43 | if (msg.status === "Finished") { 44 | document.getElementById('commit_block').style.visibility = "hidden"; 45 | } else { 46 | document.getElementById('commit').innerHTML = date + ' (' + msg.commit.subject + ')' + '
' + 'Hash: ' + msg.commit.hash; 47 | }; 48 | } else if (msg.type === 'result') { 49 | result = msg.result; 50 | 51 | $.each(result.set, function(key, value) { 52 | var bench = value[0]; 53 | date = moment(result.commit.date).format('YYYY-MM-DD HH:mm:ss'); 54 | name = xvalue(result.commit); 55 | 56 | // time chart data 57 | { 58 | item = { 59 | name: name, 60 | y: bench.NsPerOp, 61 | }; 62 | 63 | series = chart_time.get(bench.Name); 64 | if (series) { // series already exists 65 | series.addPoint(item, true, false); 66 | } else { // new series 67 | chart_time.addSeries({ 68 | data: [item], 69 | id: bench.Name, 70 | name: bench.Name, 71 | }); 72 | } 73 | } 74 | 75 | // memory chart data 76 | { 77 | item = { 78 | name: name, 79 | y: bench.AllocedBytesPerOp, 80 | }; 81 | 82 | series = chart_mem.get(bench.Name); 83 | if (series) { // series already exists 84 | series.addPoint(item, true, false); 85 | } else { // new series 86 | chart_mem.addSeries({ 87 | data: [item], 88 | id: bench.Name, 89 | name: bench.Name, 90 | }); 91 | } 92 | } 93 | }); 94 | 95 | // Now, iterate over known series, and insert nulls 96 | // if values are missing. 97 | $.each(chart_time.series, function(index, serie) { 98 | found = false; 99 | $.each(serie.data, function(idx, item) { 100 | if (item.name == name) { 101 | found = true; 102 | return false; 103 | } 104 | }); 105 | if (!found) { 106 | serie.addPoint({ 107 | name: name, 108 | y: null, 109 | }, true, false) 110 | }; 111 | }); 112 | 113 | // The same for memory chart 114 | // TODO: move to separated function 115 | $.each(chart_mem.series, function(index, serie) { 116 | found = false; 117 | $.each(serie.data, function(idx, item) { 118 | if (item.name == name) { 119 | found = true; 120 | return false; 121 | } 122 | }); 123 | if (!found) { 124 | serie.addPoint({ 125 | name: name, 126 | y: null, 127 | }, true, false) 128 | }; 129 | }); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /benchmarks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "golang.org/x/tools/benchmark/parse" 7 | "os" 8 | "time" 9 | ) 10 | 11 | // BenchmarkSet represents a set of benchmarks for single commit. 12 | type BenchmarkSet struct { 13 | Commit Commit `json:"commit"` 14 | Set parse.Set `json:"set"` 15 | Error error `json:"-"` 16 | } 17 | 18 | // BenchmarkRun represents current state of benchmark being run. 19 | type BenchmarkRun struct { 20 | Commit Commit `json:"commit,omitempty"` 21 | Error error `json:"error,omitempty"` 22 | StartTime time.Time `json:"start_time,omitempty"` 23 | EndTime time.Time `json:"end_time,omitempty"` 24 | } 25 | 26 | // RunBenchmarks loops over given commits and runs benchmarks for each of them. 27 | func RunBenchmarks(vcs VCS, commits []Commit, benchRegexp string) chan interface{} { 28 | ch := make(chan interface{}) 29 | 30 | go func(commits []Commit) { 31 | defer close(ch) 32 | 33 | handleError := func(err error, run BenchmarkRun) { 34 | fmt.Fprintf(os.Stderr, "[ERROR] %v\n", err) 35 | run.Error = err 36 | ch <- run 37 | } 38 | 39 | for _, commit := range commits { 40 | run := BenchmarkRun{ 41 | Commit: commit, 42 | StartTime: time.Now(), 43 | } 44 | ch <- run 45 | 46 | // Switch to previous commit 47 | fmt.Printf("[DEBUG] Switching to %s (%s) by %s\n", commit.Hash, commit.Subject, commit.Author) 48 | if err := vcs.SwitchTo(commit.Hash); err != nil { 49 | handleError(err, run) 50 | return 51 | } 52 | 53 | // Run benchmark for this commit 54 | // but first, try to guess what vendoring is used (if any) 55 | // and use appropriate tool 56 | runBenchmark := func(b Benchmarker) (string, error, bool) { 57 | if !b.Check(vcs.Workspace()) { 58 | return "", nil, false 59 | } 60 | fmt.Println("[DEBUG] Using", b.Name()) 61 | out, err := b.Benchmark(vcs.Workspace(), benchRegexp) 62 | return out, err, true 63 | } 64 | // Try normal go tool 65 | out, err, ok := runBenchmark(GoTool{}) 66 | if !ok { 67 | // Try GB 68 | out, err, _ = runBenchmark(GbTool{}) 69 | } 70 | if err != nil { 71 | handleError(err, run) 72 | continue 73 | } 74 | 75 | set, err := ParseBenchmarkOutput(out) 76 | if err != nil { 77 | handleError(err, run) 78 | continue 79 | } 80 | 81 | set.Commit = commit 82 | 83 | ch <- *set 84 | } 85 | }(commits) 86 | 87 | return ch 88 | } 89 | 90 | // ParseBenchmarkOutput parses raw output from 'go test -test.bench' command. 91 | func ParseBenchmarkOutput(out string) (*BenchmarkSet, error) { 92 | buf := bytes.NewBufferString(out) 93 | set, err := parse.ParseSet(buf) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return &BenchmarkSet{ 98 | Set: set, 99 | }, nil 100 | } 101 | -------------------------------------------------------------------------------- /benchtool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | ) 10 | 11 | // Benchmarker represents tool used for running go benchmarks. 12 | type Benchmarker interface { 13 | Benchmark(workspace *Workspace, regexp string) (string, error) 14 | Check(workspace *Workspace) bool 15 | Name() string 16 | } 17 | 18 | var once sync.Once 19 | 20 | // as we used cloned workspace outside of original GOPATH, 21 | // add current temp directory to GOPATH variable in order to 22 | // GO15VENDOREXPERIMENT work correctly. 23 | func honourVendorExperiment(workspace *Workspace) { 24 | once.Do(func() { 25 | if v := os.Getenv("GO15VENDOREXPERIMENT"); v == "1" { 26 | gopath := os.Getenv("GOPATH") 27 | gopath = fmt.Sprintf("%s:%s", gopath, workspace.Gopath()) 28 | fmt.Println("[INFO] Detected GO15VENDOREXPERIMENT, setting GOPATH to", gopath) 29 | if err := os.Setenv("GOPATH", gopath); err != nil { 30 | fmt.Println("[ERROR] cannot update GOPATH for benchmark") 31 | return 32 | } 33 | } 34 | }) 35 | } 36 | 37 | // GoTool is a default 'go test' tool. 38 | type GoTool struct{} 39 | 40 | // Name returns tool name. Implements Benchmarker. 41 | func (GoTool) Name() string { 42 | return "go tool" 43 | } 44 | 45 | // Benchmark runs go benchmarks. Implements Benchmarker. 46 | func (GoTool) Benchmark(workspace *Workspace, regexp string) (string, error) { 47 | honourVendorExperiment(workspace) 48 | 49 | return Run(workspace.Path(), "go", "test", "-run", "XXXXXX", "-bench", regexp, "-benchmem") 50 | } 51 | 52 | // Check guesses if it's normal go project. Basically it checks if 53 | // there are any *.go files in directory. Implements Benchmarker. 54 | func (GoTool) Check(workspace *Workspace) bool { 55 | files, err := ioutil.ReadDir(workspace.Path()) 56 | if err != nil { 57 | return false 58 | } 59 | 60 | for _, f := range files { 61 | if filepath.Ext(f.Name()) == ".go" { 62 | return true 63 | } 64 | } 65 | 66 | return false 67 | } 68 | 69 | // GbTool is a wrapper for 'gb test' tool. 70 | type GbTool struct{} 71 | 72 | // Name returns tool name. Implements Benchmarker. 73 | func (GbTool) Name() string { 74 | return "gb tool" 75 | } 76 | 77 | // Benchmark runs gb benchmarks. Implements Benchmarker. 78 | func (GbTool) Benchmark(workspace *Workspace, regexp string) (string, error) { 79 | honourVendorExperiment(workspace) 80 | 81 | return Run(workspace.Path(), "gb", "test", "-run", "XXXXXX", "-bench", regexp, "-benchmem") 82 | } 83 | 84 | // Check guesses if it's normal go progject. Implements Benchmarker. 85 | func (GbTool) Check(workspace *Workspace) bool { 86 | files, err := ioutil.ReadDir(workspace.Path()) 87 | if err != nil { 88 | return false 89 | } 90 | 91 | for _, f := range files { 92 | if f.Name() == "src" { 93 | return true 94 | } 95 | } 96 | 97 | return false 98 | } 99 | -------------------------------------------------------------------------------- /bindata_assetfs.go: -------------------------------------------------------------------------------- 1 | // Code generated by go-bindata. 2 | // sources: 3 | // assets/index.html 4 | // assets/loader.gif 5 | // assets/main.css 6 | // assets/panic.png 7 | // assets/skeleton.css 8 | // assets/warning.png 9 | // assets/websocket.js 10 | // DO NOT EDIT! 11 | 12 | package main 13 | 14 | import ( 15 | "github.com/elazarl/go-bindata-assetfs" 16 | "bytes" 17 | "compress/gzip" 18 | "fmt" 19 | "io" 20 | "strings" 21 | "os" 22 | "time" 23 | "io/ioutil" 24 | "path/filepath" 25 | ) 26 | 27 | func bindataRead(data []byte, name string) ([]byte, error) { 28 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 29 | if err != nil { 30 | return nil, fmt.Errorf("Read %q: %v", name, err) 31 | } 32 | 33 | var buf bytes.Buffer 34 | _, err = io.Copy(&buf, gz) 35 | clErr := gz.Close() 36 | 37 | if err != nil { 38 | return nil, fmt.Errorf("Read %q: %v", name, err) 39 | } 40 | if clErr != nil { 41 | return nil, err 42 | } 43 | 44 | return buf.Bytes(), nil 45 | } 46 | 47 | type asset struct { 48 | bytes []byte 49 | info os.FileInfo 50 | } 51 | 52 | type bindataFileInfo struct { 53 | name string 54 | size int64 55 | mode os.FileMode 56 | modTime time.Time 57 | } 58 | 59 | func (fi bindataFileInfo) Name() string { 60 | return fi.name 61 | } 62 | func (fi bindataFileInfo) Size() int64 { 63 | return fi.size 64 | } 65 | func (fi bindataFileInfo) Mode() os.FileMode { 66 | return fi.mode 67 | } 68 | func (fi bindataFileInfo) ModTime() time.Time { 69 | return fi.modTime 70 | } 71 | func (fi bindataFileInfo) IsDir() bool { 72 | return false 73 | } 74 | func (fi bindataFileInfo) Sys() interface{} { 75 | return nil 76 | } 77 | 78 | var _assetsIndexHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x58\x5f\x6f\xdb\x36\x10\x7f\x76\x3e\x05\xa1\x15\x93\x8d\xc6\x92\xed\x2e\x1d\x26\x2b\x02\xd6\x16\xdd\xf6\x90\xae\x40\xb6\x87\xee\x25\xa0\x24\x5a\x62\x22\x91\x1a\x49\xc5\x36\x32\x7f\xf7\x1d\xff\xc8\x96\x9c\x64\xc9\xf2\x32\x0c\x68\x0a\xb8\xf2\xf1\x7e\x77\xc7\xfb\xf3\xf3\xd9\x71\xa9\xea\x2a\x39\x89\x4b\x82\xf3\xe4\x64\x14\xcb\x4c\xd0\x46\x21\x29\xb2\x73\xaf\x54\xaa\x89\xc2\x30\xe3\x39\x09\xae\xff\x6c\x89\xd8\x06\x19\xaf\x43\xfb\x38\x9d\x07\x3f\x04\xf3\xe0\x5a\x7a\x49\x1c\x5a\xd4\x23\xf8\x9a\xd7\x84\xa9\x6b\x69\xc0\x39\x5f\xb3\x8a\xe3\x5c\x3a\xf1\x73\x0c\x98\x00\x4a\x5a\x94\x59\x89\x85\xb2\x76\x7a\x6f\x5f\x68\xa1\xe6\x79\x5b\x11\x19\x92\x4d\xc3\x85\xa2\xac\xb8\x67\xa8\xa2\xec\x06\x09\x52\x9d\x7b\x52\x6d\x41\xb5\x24\x44\x79\x48\x6d\x1b\x72\xee\x29\xb2\x51\x61\x26\xa5\x87\x4a\x41\x56\xe7\x5e\x28\x15\x56\x34\x0b\xe5\x0d\xa9\x88\xe2\x2c\xd0\x67\x2f\x35\x52\x63\xda\x19\x40\xf0\xd7\xdd\xa8\x07\xba\xc6\xb7\xd8\x4a\x9d\x8e\xfe\xbb\xc5\x02\x49\x9e\xdd\xa0\x73\xc4\xda\xaa\x5a\x0e\x0e\xd6\xb2\x15\x14\x4e\xbc\xb5\x84\x84\xcc\x17\xdf\x07\x33\xf8\x37\x8f\xde\x2e\x16\x8b\x70\x2d\xbd\xe5\xc9\xc9\x68\xb4\x6a\x59\xa6\x28\x67\x68\x73\x8b\xab\x96\x8c\x21\x4f\x35\x55\x13\x74\x07\x67\xa3\x1c\x2b\x02\x06\x6c\xdd\xdc\x51\xa0\x85\x93\x60\xc5\x45\x8d\xd5\xd8\xff\xf2\x65\x7a\x71\x31\xfd\xf0\xc1\x9f\x2c\x35\x42\x10\xd5\x0a\x86\x0c\xf0\x35\xf2\x43\x1f\x5e\x1d\xae\xc4\xb2\x0c\x64\x9b\x4a\x25\x20\xf5\xe3\xd9\xe9\x5b\x03\xd9\xe9\x28\x74\xb8\xbc\xd1\x71\xc8\x2b\x45\x6b\xed\xd4\x04\x60\x8a\x17\xa1\x3b\x93\x88\x08\xf9\xb2\x81\xe4\x12\xff\x14\x6c\x32\x46\x32\xf5\x09\x2e\x2d\x23\xa4\x44\x4b\x4e\xd1\xee\x54\x43\x14\x55\x15\x31\x10\xc8\x5a\x84\xbc\xbb\x3b\x14\x7c\xbe\x29\x3e\x61\xb0\xba\xdb\x79\x4e\x0b\xe2\x38\x56\x34\x7e\x53\xc2\xb2\xb2\xc6\xe2\x06\x8d\x99\x0c\x79\x33\xe9\x00\x9b\x1f\x37\x14\x3c\x99\xa8\xe0\x96\xb7\x44\x48\x92\x3b\xcf\x46\x96\xc1\x95\x0b\x2e\x28\xd1\x5a\xe0\xf3\x37\x30\x77\x49\xf4\xfb\xe0\xfd\xfe\x08\xfd\x85\xae\x25\x67\x57\x3a\x07\x4d\x43\x72\x88\xc8\xa2\x2b\x9c\x92\x6a\x6f\x7f\x64\xb3\xab\x88\x88\xd0\xbe\x40\xe3\x49\x77\xda\x65\xd9\x95\x4c\x95\x54\x06\xe6\xd1\xd6\x40\xe7\x74\xff\x6a\xed\x6f\xfb\xd1\x1f\xf9\x3a\xce\x83\x0e\xbc\xbb\xf5\xe3\x91\x20\x17\xc3\xc1\xb9\xae\x37\x93\xfe\x12\xdd\xf3\xae\x38\xaf\x14\x6d\x3a\x8f\x99\xe0\x52\x96\x98\x0a\xd9\xcf\x1f\x48\xc4\x30\xa3\x0f\xb8\x3e\xe4\x60\xd8\x9a\x26\x8a\xcd\x23\xad\x89\xca\x32\xaa\xeb\x48\x4a\xf4\x87\xdf\x65\xc8\x0c\x0e\xe0\xfd\x38\x4d\x74\x8f\x3a\x03\xd0\x16\xd7\xd0\x56\xfa\x2a\x71\x98\x26\x71\x2a\x50\x98\xf8\x0e\x23\xd1\xeb\x03\xa0\xeb\xf0\x27\xd4\x9c\x5d\xdd\xfb\xcf\xd7\xc6\xad\x2a\xb9\xf8\x67\x7d\x27\xb3\xc2\x57\x01\xc1\x59\x69\xb3\xd0\x70\xca\x94\x3c\x7d\xb0\x6f\xf6\xe0\xf0\xe0\x4e\xda\x1e\x65\x7a\x3e\xc0\x23\x0c\x19\x7a\xed\xd4\x47\x46\x61\xab\xc5\xc8\x4c\x43\x17\xc4\x6e\xd2\x79\x76\x5d\x20\x97\xc7\x35\x87\x11\xa0\xab\xfe\x14\x40\xab\x1f\xcd\xc5\x03\xc3\xa0\x35\x08\xd3\x4f\x9a\x19\x96\xc7\xd4\x50\x93\xfa\xbf\x60\x06\x70\xcb\xc5\xb6\xcf\x0d\xef\x1e\xa3\x06\xa0\xde\x67\xb1\xc3\x05\xa9\xff\x97\xe4\x70\x61\x52\xf1\x52\x7a\x78\xf7\x95\x1d\xbe\xb2\x43\x8f\x1d\xf6\x53\xd0\x91\xc3\x41\xf0\x2c\x6e\x58\x53\x06\xdb\x65\xc0\xcd\x7e\x09\xf5\x3a\x6e\x84\x57\x63\xff\x1b\xfd\xb1\x7e\xb5\x1f\x5d\x7f\xd2\x5b\x0a\xc7\xfd\x95\xc3\xd6\x5e\x23\x60\xdc\x9f\x02\x80\x8a\xd5\xd7\x9d\x62\xce\xba\xb5\xe5\x29\x97\xc7\x30\x4b\x69\x4f\xb8\xb5\x8b\x12\xbc\x3c\xb6\xf4\x76\x6b\xe4\x9a\xa4\x7a\x1d\x24\xc7\x4b\x76\x1c\xda\x6d\x3f\x4e\x79\xbe\xd5\xe0\x9c\xde\xa2\xac\xc2\x52\xc2\xc2\x0c\x27\x44\xe8\xcd\x75\x14\x97\xf3\xe4\x27\x8e\xde\xed\x89\xee\xf7\x5f\x00\x39\xd7\x80\x10\x10\x47\x40\xc1\xd7\x16\xd5\x93\x71\x46\xa6\xd0\x17\x22\x07\x1e\xae\xda\x9a\x79\xc8\xec\xc0\x76\x87\x9d\xe2\x8a\x16\x2c\x42\x15\x59\x29\x83\x04\x87\x8b\x64\x48\xc3\xe0\x6f\xe1\x8e\xde\x24\x9f\xb1\x2a\x2d\x5f\x82\x82\x7e\x63\x15\xde\x38\x85\xb3\xc4\x44\x6a\x35\xcc\xe3\xaf\xb6\x3e\x56\xed\xcc\x04\xe7\x02\xff\xf7\x51\x66\x40\x1d\x2e\x2f\x26\xce\x4b\x48\x71\x0b\xa4\x14\xcb\x06\x33\x44\x73\xbd\xde\x6b\x89\x67\x6e\x60\x4f\x8d\x63\x7d\x9e\x7c\xcb\x52\xd9\x2c\x63\x5a\x17\x3d\xd5\x2b\x5a\xe3\x02\x96\xab\x41\xcd\x74\xef\x12\x11\x14\x74\xa5\x2b\xe6\x6e\xef\x06\xe4\x7d\x2b\x04\x84\xf1\xde\x6c\xd0\xb6\xf3\xf5\xbd\x8d\x49\xbb\x56\x5f\xa5\x15\x14\xdc\x4b\x9c\xa6\x5b\xb6\xfb\x51\x5a\x89\x8d\x72\x60\x2f\xf8\x00\x2c\x15\x7c\x34\x7c\x88\xbc\xc5\x6c\xf6\x76\x3a\x9b\x4f\x67\x0b\x34\x3f\x8b\x66\xdf\x45\xb3\x33\x4f\x4f\xdd\xf8\x3e\xee\xd2\xf1\xe0\x6e\x37\xb1\x54\xf3\x33\x10\x98\xad\xc2\x50\x51\xcb\x0f\x29\xe9\x4a\x32\xea\x0f\xf2\xcb\xeb\x23\x60\x3e\x86\x6d\x54\x11\x06\x11\x18\xd7\x86\x51\xec\xcd\xe5\xa0\xa5\x40\x6d\x4d\xa1\x91\xc0\x91\xda\x2b\xef\x76\x3a\xf6\x47\xd2\xa1\xb3\xb0\x8f\x18\x4d\x51\x67\x01\xb8\x87\x6c\x0e\xfe\x66\xe8\xf9\x56\x06\x3d\xfc\x91\x56\xe6\x53\x4c\x83\xed\xf3\x03\xed\xfb\xdc\xf9\x4b\xb9\x80\x66\x22\xb9\x67\x4a\x3f\x24\x22\xdd\x5e\x0f\x24\x7b\x08\x19\xb0\xd0\x01\xf1\x90\xff\x15\xe7\xdd\x80\xc4\x4d\x52\x10\x46\x04\xdc\x3d\xb7\xd9\x89\xb1\xfb\x86\xeb\xbe\x8f\x17\x20\x6c\x53\xfb\x83\x00\xbd\xc5\x2c\x2c\xb8\x71\xd3\x52\xf8\x6a\x8c\x45\x41\xd4\xb9\x07\xbd\x8c\x19\x38\xdd\x1f\xc5\x21\x4e\x50\xba\x7d\xd2\xd8\x7d\x13\x46\xac\xe1\x71\xd8\xf4\xc3\xb7\xff\xc3\x07\xa7\xe1\x41\xc8\xb2\xf9\x2d\xe4\xef\x00\x00\x00\xff\xff\x5d\x85\x87\x47\x13\x11\x00\x00") 79 | 80 | func assetsIndexHtmlBytes() ([]byte, error) { 81 | return bindataRead( 82 | _assetsIndexHtml, 83 | "assets/index.html", 84 | ) 85 | } 86 | 87 | func assetsIndexHtml() (*asset, error) { 88 | bytes, err := assetsIndexHtmlBytes() 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | info := bindataFileInfo{name: "assets/index.html", size: 4371, mode: os.FileMode(420), modTime: time.Unix(1445292578, 0)} 94 | a := &asset{bytes: bytes, info: info} 95 | return a, nil 96 | } 97 | 98 | var _assetsLoaderGif = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x9c\xd8\x7b\x58\x53\x77\x9a\x07\xf0\x93\x0b\x09\xe4\x46\xae\xe4\x42\x72\x12\x20\x17\x12\x92\x10\x02\xcc\xe3\x38\x76\x07\x21\x27\x02\xa5\x10\x21\x61\xbc\xd5\x69\x29\xb5\x48\xd9\x5d\x45\xc6\xb5\x16\x15\x14\xbc\x20\xa3\x28\x41\x50\x6c\x55\x0a\x16\x21\x5c\x6c\x01\x41\x19\x0b\x4a\xf1\x52\xef\x83\x5a\x1d\xb0\x88\x48\x3b\x1d\x9e\x3e\xd5\xb2\xdd\xd9\x79\xba\xdb\x7d\x0f\x3f\xa4\x3e\xed\xd8\xcb\xf2\xa7\xfe\xc5\xfb\x39\xdf\xf7\xfd\xfe\x58\x90\xe4\x9c\xf3\xeb\x97\x35\x98\x06\x6b\xc0\x30\x3a\x95\x5e\xb2\xb9\xc4\x11\xef\x18\xba\x31\xa4\x0d\xd5\xd6\x1f\xad\xcf\xce\xca\x9e\xfc\x7c\x52\x2c\x14\x7b\xf7\x7a\xdd\xe9\xee\x8e\xae\x8e\xe1\x7b\xc3\x31\xf6\x98\x82\xb5\x05\x53\x5f\x4f\x71\x58\x9c\x94\xe4\x14\x5f\xab\x4f\xa9\x50\x1e\x3c\x70\x70\xde\xdc\x79\xe5\x65\xe5\xc3\x77\x87\xf3\xfe\x2d\x6f\xea\xf1\xd4\xb2\xe5\xcb\x7a\x4f\xf7\x8e\x4f\x8c\x17\x6e\x2c\xfc\xe6\x7f\xbf\xa1\x33\xe8\x25\x5b\x4b\x1c\x4e\xc7\xd0\xad\x21\xb3\xd5\xec\x6b\xf2\x65\xaf\xc8\x9e\xfc\x62\x52\x1c\x24\xf6\xee\xf3\xba\x33\xdd\xbd\x27\x7b\xc7\xc7\xc6\x63\x7e\x15\x53\xb8\xbe\xf0\x9b\x7f\x7c\xc3\xe1\x71\x52\x52\x53\x3a\xde\xeb\x50\xe2\xca\x83\x6f\x1f\x9c\xf7\x2f\xf3\xbe\xfd\xf6\x5b\xec\x47\x7f\x42\xbe\x65\xa7\x12\xee\x8c\x84\xf9\x2e\xc2\x6e\xb5\xd1\x28\xe4\x3f\xfd\x37\x3d\x80\x8d\xc5\x62\x66\xf2\xff\xc9\x5f\x13\x63\x5c\xec\x3b\xb0\x2a\xd1\xcc\x7a\x6e\xb0\x3e\x7f\xf1\x5f\x6c\x87\x9e\xaf\x66\x24\x5b\xe6\xce\xbd\xe9\x63\xd9\xce\xa7\xab\xdf\xdd\x51\x9d\x4c\xe1\x0f\x5e\xe3\x61\x9a\xf8\xe8\x57\x5e\x2f\x7e\xf3\xaf\xf2\x16\xf6\x9a\x87\x2f\x71\x3f\x8a\x90\x60\xe6\x8d\x49\xb9\xb2\x2d\x4e\x46\xa4\x72\x9b\xc3\x6a\x10\x96\x3b\x82\x75\x7b\x1c\xe1\xcc\xaa\x84\x9a\xfd\x55\x38\x35\xe6\xf0\x91\xba\x23\x21\x1e\xdc\x1a\xf8\x7a\x53\xb3\x8f\xef\xc2\x59\x61\x02\x91\xaf\xb3\x29\xcc\x25\xe5\x5a\x63\x63\xba\xba\x58\x2e\x3d\x2f\x9e\xf2\x41\xe7\x22\xd7\x40\xac\xdf\x39\x1f\xdb\x4d\x93\x45\xf0\xae\x34\xa9\xe5\x1e\x8e\xe0\x56\x53\x7f\x0a\xc7\xc4\x1e\x33\x69\xf8\x23\x9a\xf1\x07\x0f\x08\x8e\x78\xed\x48\x97\x5a\x17\xdc\xeb\xb4\x6a\xd4\x8f\x9a\xcd\xd2\x9e\xe1\x47\xa9\xda\xf6\x05\xaf\xfa\x6d\x67\xf8\x8b\x4c\xe5\x87\xc7\x8a\xf7\x6a\x66\x7e\xbd\xe8\xef\x7e\xbd\x52\x64\x38\x70\x66\x00\x06\x06\xe3\x9c\x65\x04\x10\x98\x34\x4c\x14\x24\x81\x17\x0c\x01\x13\xa0\xc0\x10\x4c\x00\x19\x46\x0e\x74\x00\x68\x36\x99\x49\x52\x5c\x09\x26\x48\x15\x18\x01\x1c\x01\x5e\xba\x72\x09\x3e\x10\x50\x05\x3d\xf8\x04\x40\x1e\xbe\x0b\x20\x05\x37\x00\x2c\xc8\x2f\x20\xdd\x92\x53\x00\x16\xcc\xe1\x63\x01\x6a\x30\x04\xc0\x9f\xd4\x43\x3f\x8c\x4b\x7d\xfb\x49\x9e\x17\x81\xe7\xd7\x22\xe0\xe9\xa7\x26\xeb\x92\x6d\x7b\x9b\x6e\x75\x5c\xcf\x4c\xc3\xf6\x55\x2b\x68\x5e\xfd\x03\x83\x3f\xd6\x58\x7f\xc8\xaf\x88\x39\xb5\xdc\xfa\xf1\xc8\xfa\x1b\x9e\xac\xbf\x55\xe9\x31\xee\xa6\x24\x1a\x26\x2d\x21\x2c\x38\x73\x07\x61\x0f\xf2\xdf\xe5\x30\x45\x55\x3a\xd4\x89\xd5\xf1\xf4\xda\x5a\x6b\x08\xad\xee\x9d\xfa\x77\x04\x1e\xab\x48\x9b\xe7\x6b\x69\xe5\xbb\xac\x7a\x9b\x79\x43\xeb\x89\x3c\x9b\x4b\xcc\xcd\xd9\xda\x7d\x42\xef\xb2\xa8\xe2\x29\x7d\x27\x28\xee\x0f\xa3\x19\xe7\x5b\x79\x6e\x4e\x20\xdd\x74\xd5\x87\xef\x75\x47\x9a\x6f\xfb\xce\xa4\x44\x1a\x79\x0f\x8c\x6a\xfe\x3d\xf5\xc3\xf1\x71\x22\x52\x11\x74\xaf\x0f\x0f\x53\xe6\x38\x45\x6a\xfc\x71\x4b\xb8\xf8\xd4\xc8\xb8\x6e\xd1\xaa\xdd\xac\xe2\x40\xae\xd0\xb4\xec\x7c\xa8\x5f\x80\x6e\xfe\x93\xef\x2f\xf2\x07\x40\x30\x43\x98\x24\xe8\x90\x21\xf8\x7c\x12\x8c\x60\x8c\x80\x02\x46\xa0\x00\x83\x05\x26\x98\x39\x18\xa1\x90\xa1\x6c\x91\xd1\x31\x99\x67\xe2\xb5\x64\x19\xf8\xc2\x90\x41\xe7\xbb\x6c\xdd\x18\xca\xcb\xcd\x83\xe1\x43\xbc\x80\x0f\x50\xc0\x11\x14\x00\x17\x28\x21\x88\x60\x84\x82\x05\x9f\x07\xd0\xfc\x4c\x94\xef\x01\x55\x93\x40\xb1\x00\x24\x57\x98\x0f\x5d\x3b\x13\x90\x1c\x5b\x67\xbb\xe9\xfb\xb2\x3e\x71\xe5\xfb\x62\xec\x6f\x4b\xb9\x14\x2c\xd5\xee\xa6\x36\xda\x86\xae\x0e\xf6\x60\xbf\x5f\x71\xa5\xc8\xf5\x62\x12\xb6\xea\xde\x9b\xce\x0d\x9a\x62\xa7\x1e\xd3\x49\x4b\x09\x8a\x72\xe5\xce\x04\x9c\x57\x41\x08\xc4\x5e\x47\xa8\xbe\xc6\xc1\x38\x58\xa3\xa0\x5a\xde\xa9\x6f\xa8\xc7\x17\x2b\x0c\x9c\x75\x2d\xad\xaf\xf1\xdd\x0a\x7f\xab\x40\xd7\xd6\x8d\x59\xdd\xec\x80\x8c\xbc\x9e\x56\x7f\x77\x48\x65\x24\xa5\xbf\x75\xb9\x7b\x90\x79\xa1\x25\xd0\xa3\x0e\x92\xf3\xae\xbe\xa6\x2c\xf3\xe8\x05\xb7\x5f\x3b\x9b\xa6\x37\x05\x3e\x30\xa9\xf8\xb7\x55\x24\x90\x3e\x5c\x72\xaf\x5b\xe9\x17\x9c\xe1\x34\xa8\x94\x8f\xd7\x69\xd9\xbd\x23\xbd\xc6\x9c\x55\x6d\xc9\x5b\x79\x01\x00\x24\x9b\xef\xa7\x09\x9b\x05\xb2\x3f\x23\x41\x64\x7c\xea\xea\x61\xce\xc0\x04\x43\x43\xa9\x41\x51\x42\x39\x42\xf1\x21\x23\xd3\xe4\x03\x2f\x98\x33\x30\x81\x2c\x98\x02\x13\x10\xa0\xf8\xc0\xf0\x81\x15\x31\x0d\x0c\x0e\x80\xbe\x56\x4f\x06\x13\xb0\x48\xf7\xe9\xec\x90\x69\xc2\x95\x28\x3e\xe4\x82\x6c\xf5\x01\x2e\x50\x42\x76\x40\xf0\xe7\x4b\x31\x2e\xc6\xcd\x26\xc8\xb8\x68\xee\xa1\xb7\xfa\xd6\x1c\x70\x24\x36\x7e\x1c\x36\x5a\x79\xb9\xc9\xbf\xd1\x1a\x4b\xd3\xec\x78\x57\x88\x1d\xfe\xc3\xda\xe5\x43\xe5\xab\x7e\xf3\xd7\x74\x9a\xf9\x2b\x2d\x61\x89\x9f\xd8\x49\xcd\xd5\x6c\x72\x32\x97\x96\x38\x43\x03\x36\x6e\x4f\x30\xda\xfe\x48\x30\x0d\x7b\x1c\xd2\x8a\xaa\x04\xe9\xfe\x2a\xc8\x0f\xb9\xe0\xf8\x8b\xac\xe2\xf0\xb5\x4d\xcd\xb9\x81\x19\x56\x9d\x39\xc2\xe8\xeb\xc4\xcc\x19\xaa\x00\xb1\x5d\xd1\xe5\xd3\x65\x44\x8a\x60\xc1\x35\xa7\x67\x0c\xd8\xfd\xce\x35\xb1\xdc\x3c\x19\x43\x74\x25\x17\x67\x2f\x8a\x8a\xb8\x95\xdb\x9f\x1a\x65\x60\x8d\x19\xd4\x81\xb7\xd4\xe4\x82\x8b\x52\x0a\x46\x3a\x71\x26\x2c\x38\xb1\x1a\x7f\xb4\x56\xaf\xea\x19\x1e\x91\xc5\xb4\x2f\xf8\xb0\x6f\x1b\x1d\x16\xdc\xb2\xd5\xf1\xbb\xf6\x3c\x59\x70\x96\x7f\x96\x1f\x98\x23\xcc\x1d\x6d\x22\x94\x1f\xa4\x02\x83\x86\x81\xa2\xf3\x04\x18\x30\x4d\x14\x24\xb4\xf5\x60\xa0\x28\x39\xa0\xfb\xbd\xf0\x5c\xba\x78\x09\xe6\x8e\xd8\xc8\xa3\x73\x80\x9c\x3b\xf8\xa1\x63\x54\xbe\xab\x1c\xbe\x00\x14\x21\xf8\x02\xfe\x1f\xc9\x99\xe1\xb9\xd2\xb7\x0f\x78\xba\xec\xc0\xb3\x12\xf2\xe3\xf2\x8a\x92\x63\xf7\x65\xde\x9c\xac\xc8\xba\x9e\x3b\x1f\xdb\x71\xe2\x73\x8a\xd6\xfa\x4a\xe3\x4a\x5a\x62\xc3\x11\x03\x86\xfd\x4f\xea\xf2\xf1\xa2\xe4\xe7\xde\xa8\x68\xfe\x3a\xc9\x88\xc9\xf4\x45\xc4\xeb\xbc\x82\xad\x09\x66\x8c\xb1\xd3\x49\x0f\xac\x20\x84\x1a\x2f\x11\x98\x58\x13\xcf\x5d\x7f\xb0\x42\x4e\x95\xd5\x37\x1c\x6d\x50\x2d\x96\xeb\xb9\xeb\x5a\xdb\xf2\x79\x6e\xb9\xbf\x9a\x1f\xda\xde\x83\xa9\xdd\x6c\x71\xa9\xee\x64\xbb\xbf\x5b\x22\x88\xa7\x9c\x69\xdb\xe4\x3e\x67\xd9\x70\x71\x5d\x96\x5b\xa4\x91\x0a\xae\xe7\x07\x9b\x17\x6b\xf9\x77\xf2\x07\xd2\xb4\x11\xc2\x87\x11\x4a\xde\x1d\xe5\xa7\x13\x13\x84\x36\x8c\x3e\xda\x13\xcc\x50\x94\x3a\xf5\xca\xbc\xa9\x7c\x0e\x7b\xfd\x06\x5a\x75\xdc\xfc\xd3\x26\xe9\x1c\xd7\xa2\xf3\xed\x21\x25\x12\xae\x97\x4e\x99\x21\x8a\xfa\x01\x11\xa4\x00\x66\x0c\x32\x90\x05\x98\x2b\x28\xa1\x1e\x01\x43\x85\x41\x82\x0f\x5a\x6e\x68\xad\xc1\xf8\x01\x0d\xa0\x50\x95\x80\x01\x83\x03\xd8\x02\xc5\xd3\x4a\x60\x08\xf2\x70\x50\x80\x11\x30\x21\x3e\xa8\x3e\x80\x27\xf8\x03\x14\xda\x6f\x60\x05\x5f\x01\x5a\x9b\xa8\x3e\xfc\x22\x2e\xc6\xd5\xbe\x1a\x20\x1a\x8c\x21\x13\xd4\x0e\x2b\x6e\xa1\x7f\xf2\x0b\x2b\x4c\x37\x27\x57\x11\xc6\x9c\xf7\x59\x71\xe3\xb5\xb1\x98\x30\xbb\x89\x1d\x97\xf4\x5b\x0b\x0f\x2b\xba\x9f\xb9\x6f\x7c\xcb\xc9\x74\xaa\xcb\x38\xe5\xd4\xe7\xd3\x8b\x9c\xc1\x18\xa3\xd4\xe9\x87\x85\x95\x39\xa5\xec\xdd\x04\xc7\x5a\x50\x99\x80\x6b\x6a\x1c\xb2\x83\x95\x0a\x35\x8d\x5c\x71\xaf\x78\x14\xe1\x9c\xfc\x96\xd6\x36\x9e\x5b\x61\x60\x58\xc4\x6d\xdd\x2d\x3b\x5c\xe6\xd0\xf0\x28\x5a\x4f\x8f\xc1\x1d\x02\x21\x3a\xd3\x4d\xf1\x9c\x8b\xa2\x5f\x6c\xe3\x7a\x70\xa9\x5e\x74\xbd\x45\x99\xea\x61\x59\xee\xb4\x0c\xa4\xb1\x8c\xdc\x87\x46\xd5\xea\x3b\x2a\x92\x88\x25\x59\x31\xda\xa3\x64\x06\x7f\xe0\x0c\x57\x29\xa7\x5a\x85\xe6\xc6\xf6\xbf\x3b\x77\x3b\x36\x17\x7a\x52\x0a\xf8\xfd\xed\x47\xce\x06\x19\xad\xf2\x00\xea\x33\x8d\xc8\x18\x9d\x19\x20\x7b\x82\x5e\x8b\x62\x04\x93\x47\x1b\x09\x06\x0b\xd3\xfe\xde\x19\x42\x94\xb3\x5b\x0e\x8c\xa0\xe5\x81\x32\x4c\xfb\xe9\x24\x41\x34\xc1\x0e\x15\x09\x80\x46\x6b\x10\x45\xea\xe9\x1b\x84\x10\xc1\x17\x6a\xc3\x2f\x5a\x71\xa4\xd1\xe5\x69\xa3\x93\x65\xe7\xeb\xdf\x9b\x03\x5b\x2e\xca\x0b\x35\x0e\xcf\xbc\x99\xc7\xa1\xac\xcc\x69\x8e\xe0\x3d\x60\x6f\xa0\x08\xc7\xdf\xa5\xba\x12\x23\x33\xb0\xb8\x0b\x6f\x15\x94\x8e\x85\xce\x7d\x2c\x3a\xf7\xd9\x1e\x03\xa6\x14\x6e\x22\x02\x31\x56\x29\x21\x0e\x11\x95\x11\x41\x7c\xfa\x6e\x07\x77\xb9\x37\x41\x55\x93\x50\x59\x5b\x6a\xa5\xca\xc9\x9e\x80\x7b\xac\xe2\xb0\xd5\xa8\x27\x04\x88\x04\xfa\xd6\x13\xbe\x5d\x69\x0a\x8b\x38\x4a\xd7\xdd\x9d\x93\x66\x86\x22\xd7\x7f\x22\xdc\x3d\x18\xc5\xbc\xd0\x1a\xe8\xe6\x48\xe9\x2b\xae\xad\x56\xfa\x7b\x6c\x82\x8f\x7d\x67\x9f\xb7\x19\x03\xc7\x8d\x2a\xfe\x27\xaa\x89\x87\x0f\x09\x9b\xe1\x3f\x3e\xe9\x56\x32\x82\x4f\x27\x8a\x55\xca\xaf\x5a\x58\x19\xa7\x7a\x2f\x2f\xbd\x86\xa9\x25\x54\xd5\x92\xf0\x65\xe7\x6d\xf1\x65\x82\xe4\xcd\xff\xa4\xa8\x3e\xb9\x43\x83\xd3\x4d\x6e\x82\x14\x82\x53\x84\xee\xd0\x4c\x90\xa6\x8b\x02\xba\x49\x64\xf1\x9a\xce\x12\xd9\xcf\xa0\xcf\x7d\x31\x09\x43\x47\x57\x04\x90\xd0\xe8\x51\x5d\x40\x07\x69\xa6\xcc\x5d\x24\xcb\x1c\x89\x6a\x32\xcf\x5e\x23\x94\x28\x54\x1a\x66\x5a\xdd\xda\x02\x14\x2a\xf8\x20\xe0\x23\x40\xe5\xe3\x27\xb5\x66\x8a\x76\x3e\x29\x24\x5b\x62\x39\xe4\x2a\xa6\x26\xbf\x90\xcf\x3a\xde\xb4\x36\x27\x34\xaa\x6b\x89\xdf\x4b\xac\xa3\x59\x58\xa5\xdd\xbb\x17\x73\x0f\x5d\x15\x61\x2f\x15\x45\xa8\xbe\xda\xfc\xe7\x5d\x05\x97\x23\x0b\x93\x28\x98\x7a\x41\x31\x61\x52\xf3\xb7\x25\x09\x30\x61\x79\xa2\xc8\xba\x87\xa0\x87\x56\x11\xb6\xfd\x8e\xb0\x83\x55\xca\xd0\xe9\x14\xad\xf4\x28\x23\x38\x6b\x20\x45\x3b\x5d\x4a\x43\x90\x5d\x0e\x29\x0a\x72\xb3\xb9\xa9\x2a\x32\x44\x62\x6f\xec\x96\xfe\x36\x8a\x67\x90\x71\xa1\x8d\xe7\x89\x92\x05\x6f\xbc\xba\x46\x1d\xbd\xc4\x64\xbf\xdd\x72\x36\xcd\x64\xe1\x3d\xb0\x68\xf8\xf7\x34\x50\x14\xb6\x2b\xb4\xf7\xfa\xd5\x4c\xfc\x4d\x22\x02\x8a\x76\xab\x19\x7a\xc2\x23\x57\x38\x14\x6d\xda\x74\xd1\xf6\x5e\x1f\x7b\x7e\xb6\x68\xff\x90\x67\xe6\xb1\x84\x6c\x9e\x3c\x96\x60\x3f\xc1\x1c\xd1\xaa\x43\x5b\xed\xbb\xc7\x52\x13\xf9\x58\x82\x99\xa2\x97\x12\xba\x37\x68\xa0\xa4\xd0\x04\x79\x96\x7e\xf8\x5e\x22\x2b\xfc\x93\xc7\x12\x60\x03\x2d\x79\xc0\xe6\xce\x7b\xfa\xa5\x84\x3a\x3b\xd0\xc2\x07\xf1\x73\x16\xde\x0c\xcf\x46\x92\x67\x9d\xcc\x72\xe8\xda\x81\xfb\x82\x96\xa0\x08\x59\x73\x97\x93\xc2\xe9\xf2\x08\x8b\xfc\x76\x95\x0d\x3e\x34\x70\xb1\xc3\xf5\x55\x61\xfe\xa3\x05\x85\x61\xb1\x79\xdb\x9e\x93\xbf\xbc\xe9\xbf\x08\xbf\x3c\x66\x11\x61\x51\x61\xb6\x52\x42\x69\x50\xec\x24\x6c\xfe\x15\x04\xdd\xe2\x4d\xa0\xd4\x24\x94\xd4\x96\xaa\xa9\x2a\x32\x40\xa1\x99\x6a\x4b\xe0\x74\xd1\x5e\xa8\x66\x49\x05\x22\x08\x90\x74\x61\x50\x74\xf5\xf6\xee\x13\xac\x85\xfa\xf5\xb1\x50\xb4\x71\xcf\x87\x9b\xcf\xb7\x84\x7b\x68\x32\x13\xef\x8a\x4f\x23\xcf\xe4\x6c\xb8\x95\x77\x26\x8d\x23\x0e\x1f\x13\x87\xe4\xdf\x0a\x99\x7e\x07\x05\x4b\x47\xfa\x34\x3a\x7b\x75\xa2\x25\x44\xf3\xa8\x85\x1b\x74\x6a\x78\x24\xd5\xdc\xfe\x3b\x82\xd8\x2e\x27\xdf\x41\x71\xc5\xc9\x3f\xc2\x03\xcb\x0d\x84\x50\x85\x23\x6f\x50\x56\x36\xe2\x81\x89\xa3\xfe\x06\x83\x03\x1e\x18\x25\xf0\x80\x0d\xf8\x21\x9e\xd9\xd7\x10\x98\x01\x21\x20\x01\x0f\xcc\x1d\x6c\xbe\x7b\x07\x39\x1d\x00\x03\x66\x30\x71\xf2\xba\x4d\xdb\x90\xd5\x7d\x9f\x17\xcc\xe0\x0b\x00\x1e\xe0\x04\x1e\xb0\x01\x3c\xc4\x83\x9e\x45\x3f\xcd\x73\x6d\x9a\xe7\xd6\x0a\xf2\x99\x0a\xef\xa0\x14\xa8\xd9\x96\x20\x7d\x63\xd3\x3e\xc6\xba\x95\xcd\x6a\xec\x3e\xbb\x61\x3e\x66\xb2\xbf\x1f\x4f\x95\x0d\xa5\x04\x61\xa3\x45\x91\xdc\x35\x7b\x5e\x7c\xa3\x26\xe3\xeb\xa4\x30\x2b\x66\x2c\x4a\x8a\xc4\x68\x5b\x93\x74\x98\x6d\x27\x11\x86\x69\x2a\x08\xc6\xe2\xaa\x78\x89\x74\x7f\x82\x30\xf1\xad\x8a\x68\x9a\xa5\xe1\xe8\xbb\x47\x43\x3c\xd1\x92\xf0\x35\x6d\xed\xc7\xf9\xae\xe8\x08\x86\x4c\x74\xfc\x64\xdb\xbf\xa7\x05\x69\x25\xb1\x96\x53\xa7\x22\x5c\xb6\x3f\xc4\x52\x06\x4e\xaa\xdc\x17\xe8\x97\x8e\x67\xb8\x38\x0a\x3f\xde\x8d\x36\x9c\xe5\x89\x91\xdd\x6d\x1b\x4c\x89\x11\x1b\x26\xc4\xea\xd7\xee\xaa\x3f\xfb\xf4\x53\x22\x26\x78\xdd\xfd\x53\xb8\xce\xde\xef\x94\xa8\xf1\xff\x6c\x97\x9b\xe3\x8e\x63\xad\x6f\xef\xf0\xfb\x47\xa3\xf8\xef\x71\x75\x47\xce\x6e\x93\x85\xbf\x64\x17\xd2\x67\x90\x62\x9e\x81\x84\x8e\x10\x8a\x11\xf0\x80\x0d\xba\x43\x30\x53\xd4\xeb\xc8\x2d\xf4\xd4\x11\x02\xad\x59\x24\xd8\x63\xb3\xa7\x68\x76\xb9\x91\x3d\xdb\xe9\x40\x77\x08\x05\x08\x6c\x48\x18\x54\xca\x4f\xf6\xce\x1c\xa4\xf5\x85\x4f\x5f\x23\x54\xbb\x7f\xe6\x35\x82\xaa\x4d\x22\xdd\xc8\x06\xa4\xa4\xe8\xa4\x9a\x6b\x0d\x29\xa2\x16\xc5\x52\x7a\xf3\xd7\xb4\xd1\x9c\xe6\x2a\x82\xf9\xe8\x8f\xef\x61\x7b\xec\x5e\x1a\x36\x67\xe8\xd2\xfc\x5a\xca\xef\xad\x77\x9b\x4f\xee\xbd\xb1\xf1\xb3\x2a\x26\xe6\xb7\x29\xc9\xdf\x0f\x0b\x28\x71\x16\x8a\xcb\x08\x7f\x4c\xb6\xcb\x11\x12\x5c\xe9\x10\x47\x57\x27\x84\xd4\x56\xdb\x69\xc1\x64\x86\x34\x99\x76\x89\xee\x5f\xa7\x33\x64\x67\x89\x04\x26\xc8\x90\x68\x61\x90\x4d\x12\x13\xde\xdd\xcd\x5a\x18\xb9\x31\x06\x8e\x90\x99\x5c\x71\xad\x05\x0b\x03\xe4\xf4\xc0\xab\xbe\x57\x32\xa3\x05\xb7\x7d\x67\x53\xa2\x8d\x9c\x31\x23\xce\x1f\xc1\xc9\x0c\x45\x47\x84\x8e\xf4\xab\xb4\xca\xd3\x4e\x09\xae\x7a\xd4\xc2\x86\x0c\x3d\x52\xcc\x69\xff\x5d\x69\x11\x53\x46\x66\xa8\xa2\xae\x64\x36\x43\xcf\xea\x08\x64\x41\xc8\xca\x06\x24\x10\x82\x0c\x81\x0d\x20\x91\xcb\x2d\xbf\x00\x25\x69\xb6\x23\x40\x80\x50\x41\x40\xcb\x0d\xb4\x60\xb9\xa1\xb5\xf6\xbd\x9a\x00\x19\x42\xa5\x03\x80\x41\x05\x78\x00\x06\x98\x41\x65\xb6\x23\x00\x12\x78\xa0\xe2\xfe\x8b\x3b\xc2\xb9\xe9\x8e\xa0\xd8\x7d\x5e\xbb\xfa\x70\x9e\x29\xea\x4a\xce\xf1\xfa\xf4\xc4\x36\xc5\xc0\xb8\x42\x85\xb1\x96\x7d\xfa\x3c\x75\x5d\x49\xad\x02\xc3\xd6\x66\x1e\x18\x9a\xf8\xd5\x8d\x3a\xc6\x47\xbb\x59\xd4\xb8\x37\x7f\xbb\x79\xee\xab\x1b\x16\x14\x28\x36\x3b\x8d\x18\x77\x2b\xa1\x34\x49\x77\x3a\x28\xcc\x0a\x07\xee\x4d\xd0\x65\x56\x57\x58\xa8\xf2\x43\x87\x8f\x1c\x56\xa7\x5b\x44\xba\xdc\x63\x4d\xcd\xfc\x54\x4b\x80\x44\x90\xdd\xdc\x91\x2b\x49\x0d\xb6\x2e\x95\x77\x76\x06\x64\x19\xe2\x29\x7f\xea\x28\x4d\xed\x8f\x62\x0e\x34\xaf\x7e\x81\x17\xc4\x30\x7c\x74\x4c\x75\x20\xd5\x26\xf8\xf3\xb1\x0f\x92\xa1\x1e\x7c\x02\x36\x77\xf1\xfb\xa3\xa3\x84\x2d\x5c\x72\xf7\x4f\xaa\x10\xe5\x52\xa7\x08\x2f\xfa\xe2\x18\x3b\xb8\xfb\xce\x17\x61\xdc\xaf\xea\x64\x5f\x8e\xd6\x1d\xee\xdf\x22\x48\xf7\xc6\xcd\x46\xe7\x99\x36\xa1\x4f\x6c\x8e\xd6\xcf\xfe\xad\x8e\xe4\xb1\x93\x3c\xe8\x95\x8a\x56\x19\x20\x81\x0d\x4c\x19\xfd\x95\x8e\x5c\x77\xd3\x21\x00\x21\xb2\x98\x3d\x65\x33\xd3\xd8\x9f\xc0\xa0\x33\x06\xe9\x99\x7d\x09\x91\x4b\x12\xda\x04\x74\xbf\x7b\xc3\xe8\x95\xfa\xcb\x6c\x2e\x4c\xdb\xb0\x16\x93\x7f\x46\x08\x86\xfd\xc6\x2d\x58\x32\x6e\x6b\xfc\x58\xfa\xf2\x96\x06\x13\xb6\xcb\x5a\xe0\x87\x6d\x3f\xb2\xef\xf1\xc3\x4b\x29\xb4\x79\x93\x0d\xef\x94\xaf\xff\xb0\xaa\x22\x7f\xe2\xed\xec\x62\x2c\xbd\xd0\xf9\x46\xf1\x66\x87\x0d\x13\x6e\x25\x42\xc2\x76\x3a\x44\x3a\xf6\xee\x04\x5a\xaa\x37\xfe\xf5\x1a\xaf\x99\x6a\x99\xb6\x71\x9b\x45\xda\x57\xc1\x26\xd0\x65\x66\x69\x04\xb2\xe6\x8e\x63\x1a\x97\x84\x23\x8a\x12\x77\x76\xb2\x5c\x91\xfa\x78\xca\xe9\x8e\x65\xae\xb3\x51\xcc\xc1\xe6\x32\x17\xe8\x70\x2f\x1f\x53\x55\x66\x58\x05\x37\x8f\xf5\xa5\x58\x23\x84\xf7\x23\xf0\xc0\x61\xfc\xc1\xd8\x18\x61\x95\x6f\x1a\xee\x04\x9d\x53\xa0\xa3\xfa\xb2\x49\x27\xe9\xfe\xcb\x97\xa1\xc2\xa1\xda\xac\x3e\xda\x96\x2d\xfe\x22\xe7\xa2\xf9\xf1\x49\x7c\xec\x37\xff\x17\x00\x00\xff\xff\x36\x3d\xb8\x4c\x46\x15\x00\x00") 99 | 100 | func assetsLoaderGifBytes() ([]byte, error) { 101 | return bindataRead( 102 | _assetsLoaderGif, 103 | "assets/loader.gif", 104 | ) 105 | } 106 | 107 | func assetsLoaderGif() (*asset, error) { 108 | bytes, err := assetsLoaderGifBytes() 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | info := bindataFileInfo{name: "assets/loader.gif", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1445292578, 0)} 114 | a := &asset{bytes: bytes, info: info} 115 | return a, nil 116 | } 117 | 118 | var _assetsMainCss = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb4\x92\xcd\x6e\xa3\x30\x10\xc7\xcf\xe1\x29\x46\x59\xad\xd8\x95\x20\x32\x24\x48\xbb\xe4\xb4\xb7\x3d\xb5\x87\x3e\x81\x89\x07\xb0\x6a\x30\x32\x6e\xf3\x51\xf5\xdd\x3b\x06\xd2\x84\x10\x55\x39\xa4\x58\x7c\x68\xf8\xff\xc7\xbf\x19\x4f\xa6\xc5\x1e\xde\xbc\x59\xae\x6b\x1b\xe6\xbc\x92\x6a\x9f\xc2\xfc\x3f\xaa\x57\xb4\x72\xc3\xe1\x01\x5f\x70\x1e\xc0\x67\x20\x80\x7f\x46\x72\x15\x40\xcb\xeb\x36\x6c\xd1\xc8\x7c\xed\xbd\x7b\x5e\x19\x4d\xb2\xf8\xe3\x2c\x7e\x00\xfe\x63\x83\x35\x3c\x91\xd3\x1f\x27\xe8\x8d\xad\x3c\x60\x0a\x09\x6b\x76\xc7\xc8\x16\x65\x51\xda\x14\x96\x8c\x51\x48\xc9\x1a\xc3\xf2\x18\x8a\x3b\x59\xc5\x4d\x21\xeb\x14\x62\x72\x01\xeb\x5e\x14\xb5\xb8\xb3\x21\x57\xb2\xa0\x3f\x1b\xac\x2d\x9a\x1e\x32\x76\x90\x1b\xad\xb4\x49\xe1\x47\x14\x45\xeb\xfb\x20\xc7\x7f\xa6\xc8\xf1\x14\x39\x1e\x23\x33\x5a\x51\xc7\xeb\xd0\x96\xdf\x84\x76\xa5\x9b\xd1\x15\x34\x36\x41\x4b\x8e\x64\xc9\xbd\x4e\x36\x5a\xdd\x74\xb2\x83\x6c\xd4\xa6\xd5\x00\xb3\x28\x91\x0b\x34\x8e\x68\x2b\x85\x2d\xbb\x62\x7e\x92\xba\xe1\x42\xc8\xba\xa0\x4a\xdc\x1c\x9c\x25\x08\x33\x6d\xad\xae\x48\xd8\x15\x34\xa3\x9d\xb3\x67\x69\x29\xbc\x0b\xdb\x92\x0b\xbd\x4d\x9d\x1e\x96\x74\x93\x02\x4c\x91\xf1\x5f\x94\x34\x80\xd3\x83\x2d\x56\x7f\x7f\x3b\x6f\xa5\x0f\x23\x23\x5d\xb7\x7a\x2f\x6c\x70\xee\x8d\xbf\xf6\xba\xba\x73\xad\x6d\x5f\xf7\x50\x96\xd5\x4d\x3a\x8c\xcf\xa9\xf8\x64\x32\xfd\xc6\xf5\xf4\xe2\x18\xdc\x18\xce\x4e\xc3\xc6\x72\xb7\xfa\x6d\x32\x6d\xa8\xbd\x28\xdc\x46\xfd\x37\xe9\x89\x4e\xb9\x34\x85\xc1\x3d\xb4\x5a\x49\xe1\xc4\x1f\x01\x00\x00\xff\xff\x97\x8f\xa0\xbf\x3a\x04\x00\x00") 119 | 120 | func assetsMainCssBytes() ([]byte, error) { 121 | return bindataRead( 122 | _assetsMainCss, 123 | "assets/main.css", 124 | ) 125 | } 126 | 127 | func assetsMainCss() (*asset, error) { 128 | bytes, err := assetsMainCssBytes() 129 | if err != nil { 130 | return nil, err 131 | } 132 | 133 | info := bindataFileInfo{name: "assets/main.css", size: 1082, mode: os.FileMode(420), modTime: time.Unix(1445292578, 0)} 134 | a := &asset{bytes: bytes, info: info} 135 | return a, nil 136 | } 137 | 138 | var _assetsPanicPng = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x74\x54\x7b\x4c\x53\x67\x14\xff\x4a\x81\x02\x05\x44\x85\x2d\xe2\xa6\x15\x4b\x87\x62\xb1\xa2\x10\x44\x93\x59\xb9\x05\xaa\xb6\x01\x8a\x82\x31\x92\x96\x72\xc1\x32\x44\xa3\x9d\x0a\xea\x04\x04\x59\xe3\x06\x15\x90\x4c\x31\xda\x16\x18\xc2\x80\x01\x3e\x98\x4a\x16\x40\x1e\xa2\xe2\x03\x1f\x08\x48\x86\x40\xd1\x76\x44\x7c\x6c\x73\x99\x8b\xbf\xdd\x9b\xfd\xb5\x3f\xf6\xe5\x3b\xe7\x77\xce\xb9\xbf\x73\x73\xce\x3d\x27\xd7\x10\xa3\x8c\xf2\x70\xf3\x75\x23\x84\x78\xc8\xa3\xa9\x38\x06\x3f\x61\xc5\x85\xcb\x68\xd9\x83\xe5\xdf\x33\xe0\x98\x26\x55\x48\x09\x69\x2e\xe6\xff\xad\x71\x62\x7c\x81\x36\x3a\x4e\x41\x48\xb6\x88\x90\x9c\x7c\x42\xfe\x62\x42\x39\x2f\x08\xf9\x52\x42\x88\x5d\x4d\x48\xf8\x77\x84\x7c\xbc\xfb\xcc\xd5\xd8\xb5\x84\x70\xa6\x62\x36\xc5\xcb\x18\x02\xfc\xfd\xc1\x1c\xa1\x10\x8b\x16\x61\xf1\x62\x88\x44\x30\x9b\x31\x34\x84\x89\x09\xf6\x32\xb6\xc9\x04\xab\x15\xe7\xce\xb1\xc6\xd4\x14\x6b\x30\x97\x89\x30\xee\xf0\x30\xcb\x9c\x9c\x64\x99\xe3\xe3\x78\xf6\x0c\x1d\x1d\x38\x7a\x14\x9d\x9d\xb8\x7c\x19\x0b\x17\x82\x79\xbf\x40\x80\xd6\x56\x38\x39\xc1\xd5\x15\xce\xce\xe0\x72\xe1\xe8\x08\x07\x07\xcc\x9e\x0d\x37\x37\xe4\xe7\xc3\xdd\x1d\x67\xce\xa0\xbd\x1d\x17\x2e\x60\xc3\x06\xec\xdf\x8f\x23\x47\x50\x58\x88\xcd\x9b\xb1\x73\x27\x94\x4a\x24\x27\x83\xa6\xb1\x6d\x1b\xee\xdd\x43\x7d\x3d\x0a\x0a\xf0\xe4\x09\x5b\xc9\xf5\xeb\xf0\xf4\x44\x7f\x3f\xbc\xbc\x70\xed\x1a\x1a\x1b\x51\x5a\x8a\xf4\x74\xf8\xf9\x61\x70\x10\xb7\x6f\xe3\xf9\x73\xcc\x99\x83\xee\x6e\xac\x5b\x07\xa9\x14\x8f\x1e\xb1\xdd\x31\x6d\x32\x35\x8f\x8d\x41\x2c\xc6\x95\x2b\x38\x71\x02\x3a\x1d\xd4\x6a\xf8\xf8\xa0\xab\x0b\x21\x21\x08\x08\x40\x54\x14\x12\x12\x70\xe9\x12\xde\xbd\x83\x56\x0b\xa3\x11\xc7\x8e\xc1\x60\x40\x51\x11\x6e\xdd\x42\x59\x19\x6a\x6b\x71\xff\x3e\xbc\xbd\xb1\x6f\x1f\xca\xcb\x51\x53\x83\x53\xa7\x70\xfa\x34\x0e\x1e\xc4\xc0\x00\xde\xbf\xc7\xcc\x0c\xb2\xb2\x50\x59\x89\x9c\x1c\xb4\xb4\xc0\xc3\x03\x87\x0f\xa3\xaf\x0f\x6d\x6d\x48\x4b\x43\x45\x05\x7a\x7b\xb1\x6c\x19\x5c\x5c\xf0\xea\x15\x1a\x1a\xa0\x52\xa1\xb9\x19\x6f\xdf\xe2\xf8\x71\x14\x17\x83\xc3\x41\x58\x18\x82\x82\x60\xb1\xb0\x5f\x7b\x74\x14\x14\x85\xc8\x48\x64\x66\xb2\xc3\xda\xba\x15\x7c\x3e\xf4\x7a\xd8\xed\xec\xe0\x6c\x36\x64\x67\x63\xc9\x12\x6c\xdf\x8e\x8c\x0c\xec\xd9\x03\x1e\x0f\x0a\x05\x92\x92\x50\x5d\x0d\x5f\xdf\xba\xb3\x69\x9f\x32\x93\x9e\xaf\x8f\x53\xaa\xc8\xbf\x47\x39\x62\xb7\x6d\x8a\x18\x89\xa0\x9e\x52\x9b\x86\x15\xc3\x43\xf6\x17\x76\xfb\x7e\x87\xc2\x9f\x99\x47\x9c\xe4\x8d\x51\x94\x43\x2a\xff\x50\x0a\xe3\xb8\xee\x8e\xde\xba\x97\xd9\xc0\x41\x56\x38\xeb\x03\x2a\x0e\x30\x84\x19\x39\x25\x8d\x0f\xb8\xab\x55\x13\x4e\xa4\x68\x9f\xa7\x32\x28\x60\x6c\x72\x2c\x35\x39\x25\x87\x1b\x29\x5a\x90\xa4\x9b\x17\x66\xb4\x5a\x23\x6e\x04\xaf\x7c\xcd\xbf\xd1\x17\xd8\x9e\x6b\x29\x21\xb3\xe6\xae\xa4\x8a\x0a\xe4\x31\xeb\x5c\xe5\x4a\xa9\x34\x49\x9e\x53\x6c\x70\x12\xf6\xde\xec\x2a\x14\xf1\xca\xad\x3c\x1b\xdd\xe5\xc7\x3b\x49\x6b\xd5\xe3\x1d\x2c\x7a\xcd\xe9\x11\xf1\xca\x4e\x4e\xf8\x68\xbc\x4e\x6e\x89\xdf\x12\xeb\xfc\xa6\x2c\x37\xbf\xee\x74\x5e\x4b\xb8\xd1\x6c\xae\xad\x16\x2c\x17\xf7\x08\x1a\x1b\x2a\x7f\xa8\xd9\x65\x32\xa9\x77\x54\x99\x33\x33\x2c\xea\xf4\xd4\x4c\xb3\x5a\x47\x8b\x7f\x54\x2f\xee\x10\x04\x55\xea\xe8\x2f\xd2\x66\x4c\xf4\xcc\x8b\xaa\xe9\x69\xcb\x4b\x7b\xaa\x76\xfa\xe5\x33\x1b\x5d\x6f\xf2\x28\xf9\xed\x8f\xd7\xbf\xbf\x9d\xe5\xe2\xee\xe6\x19\xcb\xbf\xc4\x77\xe1\xb7\x36\x1b\x8c\x45\xfa\xbd\xb1\x8f\xcb\x1d\xe5\x91\x32\x5e\x62\x77\x97\x5d\xde\xab\xeb\xfa\x4c\x28\x2a\x18\x11\xd6\x54\x2d\x3d\x70\x2d\x70\xc7\xc6\xdc\x6f\x14\x1b\xef\xde\xb8\x7d\xf3\x8e\xeb\xca\x2d\xad\xe5\xfd\xfd\xfc\x56\x79\xdf\x72\x72\x25\xa1\xa2\x34\x57\x15\x79\xa1\xe9\xfc\x4f\x54\xc3\xc5\xfa\xaa\xb6\xce\xab\x2d\x75\x57\x7b\x44\xd5\x1f\xf9\x2d\xb9\x23\xf0\x9f\xd8\x9e\xa0\xfa\xc5\x74\xea\x71\x62\xc6\xfc\x27\x0f\x9f\x0e\xa4\x6b\xb4\x4f\x1f\x37\x7a\xcd\x1d\xe4\x94\x95\x39\x0e\x0f\xd5\x9a\xee\xd1\x23\xa3\x0f\x46\xd2\x46\x3f\xd7\x58\x0a\xbe\x55\x58\xf4\x55\x9a\xf3\xc1\x31\x71\x0e\x7e\xab\x6b\xb3\x5b\xea\x3e\x78\xff\xfa\xc1\x58\x1b\x12\xda\xf3\x20\x6f\x6d\xcd\x5d\x32\x2f\xd0\xdc\x50\xba\x4a\x25\xd5\x94\x6b\x0a\x3b\xa7\x57\xd3\xf7\xf7\x8e\x3c\xe7\x70\x35\x5f\x4d\xe5\x15\xb8\x85\x8e\xbb\x27\x0f\xbc\xb9\xe9\x6b\x6c\x57\x25\x38\x39\x9f\x5b\x2a\xfc\x33\xde\xdb\xbb\x5a\x36\x8f\x3a\x60\x6d\xe2\xb5\x95\x34\x79\x71\x25\x86\xb1\xaf\x83\x17\x88\x9c\x43\xeb\x5b\xc6\xf2\x3b\x5d\xe4\x49\x89\x25\x06\xc1\xdc\x40\x61\xee\x6b\x8d\x48\x22\x8d\x76\xe0\xda\x9c\x44\x3e\xb2\x49\x8a\xc8\x93\x9a\xa8\xfe\xde\xc0\xe3\xd1\x0f\x07\xda\xd6\x9e\x97\x9f\x9d\xbf\x7a\x4d\x53\x84\x95\x88\x0f\x1d\x4a\x3c\xb2\x26\x9c\xfd\x15\xf9\xeb\x65\x89\xfa\x14\x8d\x9e\x0e\xd7\xee\xa1\x19\x20\xc1\x92\x15\x21\xe2\x15\x12\xf1\x8a\xb0\xf8\xe0\xe0\xf0\x55\xa1\xe1\x12\x89\x58\xb2\x8a\xd1\xdd\xd3\x1d\xcb\xfe\x93\xb0\x73\x57\x8a\x2e\x35\xeb\xff\x13\x2e\x36\x67\x19\xd9\xd5\x94\xcb\x94\x54\xe3\x7a\x75\xde\x3f\x01\x00\x00\xff\xff\x4e\x03\xf2\xfa\x01\x05\x00\x00") 139 | 140 | func assetsPanicPngBytes() ([]byte, error) { 141 | return bindataRead( 142 | _assetsPanicPng, 143 | "assets/panic.png", 144 | ) 145 | } 146 | 147 | func assetsPanicPng() (*asset, error) { 148 | bytes, err := assetsPanicPngBytes() 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | info := bindataFileInfo{name: "assets/panic.png", size: 1281, mode: os.FileMode(420), modTime: time.Unix(1445292578, 0)} 154 | a := &asset{bytes: bytes, info: info} 155 | return a, nil 156 | } 157 | 158 | var _assetsSkeletonCss = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xcc\x3a\xdb\x8e\xdb\x38\xb2\xef\xfe\x0a\xa2\x07\x03\x4c\x02\x5b\xed\x7b\xba\xdd\x98\x83\x93\x4b\x77\x66\x81\x49\x82\x4d\xb2\xbb\x0f\x8b\x79\xa0\x25\xca\x22\x5a\x12\x35\x24\x15\xb7\x27\x18\x60\xff\x61\xff\x70\xbf\x64\xab\x28\x51\x37\x93\x8e\xf7\xc9\x71\xd2\xb0\x55\xc5\x2a\x16\x8b\x75\xa5\x78\xfd\x7c\xf4\x9c\x7c\x7a\x64\x29\xd3\x22\x27\x7f\x9f\x07\xd3\x60\x09\x90\xd7\xa2\x38\x48\xbe\x4b\x34\x99\x4f\x67\xcb\x31\x79\x43\xbf\x30\xf2\x96\x66\x34\x4c\x18\xa0\xf7\xfb\x7d\xb0\x63\x5a\xd5\x74\x41\x28\x32\x80\x3e\x48\xc6\x88\x16\xa4\x54\x8c\x94\x79\xc4\x24\xd1\x09\x23\xef\xfe\xf2\x99\xa4\x3c\x64\xb9\x62\x01\x0c\x4a\xb4\x2e\x36\xd7\xd7\xc8\x41\x14\x00\x14\xa5\x0c\x59\x20\xe4\xee\xba\x1e\xa4\xae\x33\xae\x27\x96\xa2\x48\x0a\x20\x9a\xcd\xaf\xe7\xb7\xd7\x28\xca\xe8\xf9\xf5\x68\x34\xba\x7e\x4e\x3e\xd3\x6d\xca\x88\x88\x49\x28\x72\xcd\x72\xad\x46\xff\xf9\xd7\xbf\xbf\xc3\xff\xa3\x09\x79\x2b\x79\x04\x5f\xaf\x28\xe8\xe5\x93\x3e\xa4\x4c\xc1\xd3\xe7\x43\x21\x76\x92\x16\xc9\x01\x1e\x7e\xe5\xf9\x23\x02\x5f\x95\x1a\xd4\x89\xbf\x1e\x84\xcc\x94\xc1\x28\x8d\xdf\xaf\x45\xc4\x90\x0a\x57\x8d\xcf\x9f\x0a\x1a\xf2\x7c\x07\xbf\xfe\xa6\x79\xca\x35\x37\xd0\xd7\x29\xa3\xb2\x02\xbf\x63\x11\xa7\xe4\xaf\x25\x93\x88\xb2\x5a\x33\xa2\x5c\x5c\x27\xce\xff\x04\x64\x0c\x70\x33\x29\xcf\xc1\x74\xbe\x8e\x08\x29\x84\x82\x95\x89\x7c\x43\x24\x4b\xa9\xe6\x5f\xd8\x1d\x40\xf7\x3c\xd2\xc9\x86\xcc\xa6\xd3\x1f\xf1\x31\xa3\x4f\x93\x1a\x74\xbb\x9e\x16\x4f\x15\x4c\xee\x38\x90\x4d\x09\x2d\xb5\x40\x48\x41\xa3\x08\x14\x83\xa0\x79\x3d\x68\x2b\x9e\x26\x8a\xff\x61\xa0\x5b\x21\xc1\x5e\x27\x00\xba\x23\x7f\xa2\x18\x69\x99\xe5\x63\xfb\x43\x19\x69\x06\xf3\xc6\xa9\xa0\x7a\x43\x52\x16\xeb\xd3\xdc\x50\xef\xb0\x9d\x24\x62\x5f\xc0\xa8\x15\x49\x41\x38\xe3\x1b\x34\x27\xcb\x29\x08\x83\x2b\xff\xff\xcc\xec\xd7\x4f\x19\xcf\xed\x6a\x0c\xee\x99\x99\x7a\xa0\x97\x46\x96\x9b\x95\x11\xa5\xbb\x3c\x9c\xf1\xf4\x9c\xab\x95\x7f\x4e\x83\xfb\xc6\x9c\xb0\x7c\x98\xc3\x0c\xa8\xb4\xd4\xfc\x54\xf5\xc8\x4a\xfd\x13\x54\x0d\x2c\xa3\x3f\x7c\x13\x73\xa9\xf4\x24\x4c\x78\x1a\x75\x49\xbb\x70\x17\x1b\xb3\x30\x1c\x2f\x72\xd6\x9d\xb9\x7d\x54\xc4\xf1\xf9\x6a\xc5\x5e\x06\xeb\xe6\xf3\xc2\x8a\xa4\xf7\xe2\x2c\xe2\xd9\x22\x58\x34\x9f\x86\x38\x81\x78\xe7\x27\x6f\x88\xe7\x73\xa0\xe8\x7c\x0c\x71\x0c\x71\xef\xc4\xd4\x0d\xf1\x62\x1a\x1c\x8b\x1d\x83\x27\x9c\x45\x7c\xeb\x10\x5b\xf1\xa7\xf3\x14\x76\xe3\x10\x5b\xb1\x2f\x2c\x3f\x63\xcd\xab\xb5\x43\x6c\x86\x09\xe5\x0c\xe2\xf5\xca\x21\x76\xce\x4f\x6e\x74\x43\xfc\x62\x19\x4c\xbb\x82\x57\x5b\x75\x4a\xe8\x0e\xf1\xcd\xdc\x25\x76\x7a\x72\xd1\x0d\xf1\xed\xcc\x65\x24\x7b\x96\x9e\xd8\xac\xaf\xbd\xa8\xe2\xb7\xf8\x89\x4e\xb8\x8c\x6a\x36\x3e\x1e\x4e\x5b\x01\x13\xaf\x88\x95\x93\xfa\x84\xd2\xed\xd4\x09\x4d\x63\xf7\xcc\x03\x63\x31\x24\x10\x78\x3e\xc4\xb1\x82\xe2\x00\x43\x0c\xb2\x30\x4f\x93\xed\x61\x32\xf4\x5c\x07\x42\x75\x79\xf7\xb4\x71\xe3\xf2\xdf\x96\x45\xeb\xc9\x63\x1f\xc2\xcf\x7b\xf6\xc2\xb1\x73\x1d\x16\x1d\x47\x1f\x72\x77\xc5\x80\x01\xf7\xf9\xda\xe1\x48\x2d\x8b\x4e\x24\x18\x7b\x31\xca\x27\xfa\x62\xe9\xd8\xf3\x0e\x8b\x36\x52\x0c\x99\xbb\x62\xc8\x80\xf9\xd2\x15\xf6\x5a\x16\x6d\x24\x19\xfb\x10\x7e\x9d\xaf\x5c\x51\xb1\xc3\xa2\xe3\x73\x43\xee\x2e\x77\x1c\x70\x5f\xbb\x5c\xa1\x65\xd1\x8d\x44\x63\x3f\xca\xcb\xdd\x15\x57\x5b\x16\x9d\x50\x35\xf6\x62\xbc\x4a\x7f\x71\xe3\x08\x60\x1d\x8b\xf3\xa9\xc5\x19\xe3\x86\x4e\xe4\x0a\xcb\x9d\xb5\xa7\x7e\xad\x3b\xa3\xe0\x80\xfd\xad\x3b\x86\x74\xfd\xbc\x17\xc8\x8e\xc3\x40\x0f\xad\xfe\x77\x7b\x3f\x8a\x76\xc7\xd1\xa0\x8f\x57\xe7\xec\xee\xb1\x98\x9d\x98\xe8\x58\x44\x07\xab\xbc\xb6\x0f\x6c\xff\xac\x8a\xf2\x6e\x63\x70\xf1\x32\xdc\x5b\x9b\x83\xa0\xef\x3f\x7c\xbe\x1f\x25\x3a\x4b\x09\x57\x04\xd6\x8b\xcd\xde\x7a\x1e\xac\x7e\x24\x4a\x60\x6d\xa9\x09\x4d\x53\xd3\xf4\x7d\xbc\x7f\x47\x32\x46\x55\x29\x59\x86\xcd\x19\x00\xa5\x28\x77\x89\x28\x75\xd3\x6d\x8e\xa8\x64\x64\x0b\x6b\x8f\x08\xb4\x9e\x33\x2c\x4a\xab\x02\x3a\x20\x9f\x04\x22\x78\x08\xec\x0e\x64\x16\xac\x80\x0b\xf9\x99\xcc\x56\x30\x64\xf3\x0c\x85\x31\x42\x60\x95\x18\x43\x91\x8a\x75\x37\xdb\x54\x92\xa0\x5e\xb7\x22\x3a\x0c\x91\xc0\x84\x65\x77\x98\x9d\xc2\x52\x4a\x10\x09\x18\xb3\x4c\x91\x90\x62\xb7\x1a\x82\x74\x19\x08\x53\xee\x48\xc6\x15\x87\x86\x52\x16\x92\x69\x90\x05\x3a\x0f\x18\x05\xf2\x19\xa6\x20\x38\x2e\xa7\xca\x6c\x29\xc7\xad\x36\xf1\x02\xf9\xaf\xef\xec\x8c\xfb\x1a\x06\x05\x7c\x03\x8b\x69\xc6\xd3\xc3\x86\x5c\x7d\xa4\x29\xdb\xd3\xc3\xd5\x98\x5c\xfd\x82\xa5\x81\x86\x55\xbe\x67\x25\xeb\x01\x48\x0d\x69\x00\x63\xf2\x52\x72\x9a\x8e\x89\xa2\xb9\x82\x08\x28\x79\x8c\xac\xc1\xc6\x84\xdc\x90\x1f\xe6\xf3\xb9\x31\x28\xd3\x18\xb7\x8d\xe5\xc5\xed\xc6\x6b\x4c\xc9\x6c\x4c\x92\x39\xfc\x2d\xe0\x6f\x09\x7f\x2b\xf8\x5b\x9b\x4d\xab\x3d\x45\x8b\x02\x4b\xa0\x16\xb0\x15\xd0\x1b\x67\x90\x4e\x61\x47\x8e\x54\xbd\x98\x9a\x6a\x29\x99\x81\xab\x75\x76\x1d\x6a\x41\x1c\x3d\xdc\x2a\x50\x16\xf4\x6d\x1a\x76\x79\xa2\xaa\x3e\x7a\x43\x26\xc1\x0c\x87\x02\x8f\x79\x9f\xc7\x22\x58\x3b\x79\xac\xee\x7c\x3c\x50\x90\xc5\x90\x89\x53\x90\x85\x5f\x10\x64\xb2\xec\x33\x99\x07\x4b\x27\x13\xa7\x24\xd3\x1b\xcb\x65\xd5\xe7\x32\x0b\x6e\x5c\x5c\x56\x4e\x51\xa6\x2b\xcb\x65\x3d\xe4\xb2\x72\x71\x59\x3b\xb8\x4c\x6d\x13\xfc\x6b\xa7\x09\x2d\x12\x3c\xc9\xd0\xdf\x6e\x43\x87\x5b\xba\xaa\x35\x89\x81\x7f\xb8\x55\xcb\x60\xde\xe2\x8e\x76\x60\xdd\xe2\x96\xee\xdd\x31\xb8\x95\x5b\xe9\x06\xe7\x51\x82\xe9\xb8\x0b\x87\xf5\x5a\x9f\xac\xce\x77\x2e\xee\x79\x5e\x77\xa4\x46\x76\x1b\x4d\x66\xf7\x2f\xef\xdf\xbc\x42\xe1\xe9\x26\x11\x5f\xea\x03\x00\x8b\x9d\x3e\xbc\x9c\xbe\xbe\x6f\x96\x66\x8f\xac\x2e\xbe\x0e\xef\xe2\x82\xad\x11\x71\x3c\xb2\xdf\x3c\x2f\x4a\xfd\x4f\x7d\x28\xd8\xcf\x57\xaa\xdc\x66\x5c\x5f\xfd\xd6\x87\x4a\x06\xd9\x6d\x08\xac\xc8\xaf\x7e\x33\xda\x88\xb8\x2a\x52\x0a\x01\x9d\xe7\xc6\x05\xb6\xa9\x08\x1f\x31\x2c\x59\x5f\x58\xdc\x54\xa7\x4c\x9d\x83\xa7\x45\x7d\xf0\x64\x35\xb9\x5a\xad\xf0\x51\xb3\x27\x3d\xa1\x29\xdf\xe5\x1b\x12\x32\xcc\x3d\x77\x83\xdc\x35\xab\xe8\x7a\x11\x6f\x5d\x25\x97\x9e\xff\xd9\x39\x87\x1e\x58\x45\x14\x3b\x95\x96\x90\x43\x62\x21\x21\x96\x96\x45\xc1\x64\x08\x29\xb8\x41\x46\x2c\x14\x92\x56\x87\x6e\x39\xd4\x32\xe6\xc0\x2d\xe1\x9a\x19\x6e\x0c\x81\x7b\x48\x2e\xe6\xc4\x8b\x86\x8f\x3b\xc8\xea\x79\x34\xa9\x57\x64\x38\x17\x14\xd3\x6b\x75\x24\x66\x8e\xc1\x24\x8d\x78\xa9\xc0\x3b\xed\xb1\x1b\x42\x61\x55\x98\xef\x45\xca\x23\xf2\xc3\x76\xbb\x35\x7a\x29\xa5\x42\x36\x85\xe0\x56\x0b\x27\xce\xe8\xaa\xed\xa8\x0c\xd4\xee\xad\x7d\x72\xed\xb0\x0b\x57\xef\xb3\x0b\x65\x77\xdb\xe2\xec\x7c\xb1\x08\x4b\xd5\xcc\x57\x3f\x39\xe7\x73\xe0\xec\x7c\x0e\x54\x33\x9f\xc1\xf5\x3c\x0e\xca\xd0\x8e\x3a\x2d\xf4\xe6\xe6\x06\xa1\x50\x51\xa1\x0d\xd4\xd1\xa6\x96\xb2\xfe\x9a\x14\x92\x43\x40\x3a\x58\x71\x8f\xc0\x2e\xb9\x4f\x0e\xaa\x17\x70\x72\x8c\x5d\xc9\x60\x50\x6f\x49\x0f\x0f\x0f\x6e\x13\x82\xc5\xbe\x5e\x3c\x4c\x1d\xeb\xad\x11\xde\x45\xf6\x2d\xc1\x83\x3c\x63\xc1\x27\xec\xe4\x8c\x91\x9e\xc5\x0f\xac\x68\x88\xed\x19\x95\x07\x79\x8e\xe8\x7e\x93\x3b\x63\xa4\x4f\xf4\x63\x83\xf4\xef\x5e\x9d\x3a\x8e\x77\xaf\xcd\x29\xf6\x6c\x3a\xfb\x9e\x73\x46\x57\x2f\x2c\xa3\x3c\x1d\x26\x83\xbc\xcc\xb6\x4c\x0e\xa1\x8a\x51\x19\x26\x43\x28\x06\xd6\x63\xd8\x11\xcb\x52\x1e\x81\x0a\xaa\xd4\x1e\x14\x89\x70\xe4\x02\xa1\x95\x8e\x47\x0a\xda\x90\x50\x57\xd5\x91\x2f\xdb\xac\x21\xba\x62\x4b\x65\x5a\x9e\xcf\xd0\x91\x21\x00\x4c\x50\xd7\x4d\x55\x95\x68\x94\x09\xfa\xd8\xde\x3c\x3c\x8c\x09\x64\x20\x21\xa1\x1b\xdb\x1e\xc8\x3f\xd8\xf6\x91\xd7\x7d\x8e\x63\x9b\xe3\x38\xf6\x84\xf2\x37\x33\xfc\x77\x2a\xfa\x43\x40\x4f\x68\x24\xf6\x6d\x7e\xf1\x07\x79\x90\xfd\x23\xcb\xc0\x75\x14\xa1\xfb\xc7\x3d\x95\x11\x89\x58\x4c\xcb\x54\x13\x65\xfa\x65\x14\x5d\x61\xeb\x66\xb4\xa6\x20\x45\x4a\xc2\x3f\x7c\xfa\xce\xb7\xd0\x6c\x1d\x64\x72\xd4\xf1\x84\x42\x0e\xa6\x90\x38\xab\xe4\x5a\x69\x04\x3e\x93\x4c\xfc\xe1\xc3\x99\xcf\x11\x0e\xf4\xd5\x9b\x00\xeb\x69\x6b\x1f\xeb\x55\xcf\x3e\xaa\xfa\x74\xdd\x87\xd9\x06\x0b\xc1\xc0\xcb\xa1\x3f\x57\xd4\xb0\x5a\x74\x86\xa9\x5a\x97\x2e\x5c\xa5\x51\x37\xc6\x33\x95\xd1\xae\x0b\xd1\xea\xd8\x62\xad\x22\xec\x73\xe5\x32\x9d\x50\xe6\x30\xdd\x36\xf3\xf4\x73\x6a\x4a\xb7\x2c\x1d\x8f\x52\xb6\x63\x79\xd4\x2f\xfe\x9a\xaa\x6f\xd0\xa3\x56\x7d\x81\xab\x64\x03\x7e\x31\x67\x69\x84\xa7\x27\x5f\x7b\xe5\x61\xc7\x67\xea\x2e\x68\x3a\xdc\x86\x30\x61\xe1\x23\xb8\xc6\x51\xb9\x0a\x3e\x26\xdc\x85\x69\xb3\x00\xf2\x7f\x24\x30\x3f\x26\xcd\xf1\x88\xb7\x86\xed\x1d\x56\xb9\xd7\x02\x91\x22\xa3\x69\xa7\xc1\x51\xdf\xed\x1b\x78\x0c\x06\x65\x75\x5a\x94\x82\x98\x13\x13\x39\xa0\xd4\xe6\x32\x4c\x31\x70\x28\x1e\x19\x35\x89\xe3\x31\x50\x0d\x43\x0e\x4c\x7b\x83\xc6\xa4\x66\x66\xfd\xc6\xbe\xad\x71\x75\x7f\x30\xb4\x04\xdb\x81\x2f\x20\xc4\x19\xea\xaf\x9a\x85\x7d\x4f\x5d\x1f\x71\x4d\xed\x8f\x45\x57\xe5\x55\x13\x70\x5b\xbd\x76\x4d\x79\xb7\xcd\xb4\xf6\x66\x4f\x0c\xcc\x5e\x98\xab\x02\x17\xd7\xba\x77\x2b\x42\x10\xaf\x6f\xfa\xa6\x6b\x6f\xed\xac\x7d\x79\x1f\xcc\xdd\x7a\x38\xa7\x27\xc1\x32\x65\x86\xff\x3c\x89\xea\x7e\x86\xff\x3c\x89\x0a\x54\x59\x48\x06\x2e\xd3\x08\x7b\xec\xf2\x8d\xf8\xa8\x7c\x7b\x14\x30\x94\x0c\xb8\xb4\x07\x73\xd5\xdd\x8d\x8b\x6f\x82\x77\x67\x74\x02\x81\x33\xea\xef\xcd\x6c\x8e\x85\x44\x9d\x3d\xba\x9d\x6a\x7b\xef\xa1\x4e\xd8\xb5\x25\x1e\x69\x18\xd3\x52\xd2\x7f\xeb\xaf\xa3\xa3\xb7\xfd\x43\x67\xaa\xa8\x52\xda\x23\x6a\x1f\x7b\x34\xb2\x0a\x49\xed\x71\x8b\xbd\x1c\x73\x71\xad\x7a\x55\x6d\x8f\x22\xea\x5a\xfb\x94\x4f\x9b\x40\x7f\x5c\xfe\x8d\xfb\x79\x64\x48\xdc\x9c\x4c\x81\x05\x42\x63\x81\x46\xfb\x7b\x29\x34\xfc\x8e\x52\x24\xdd\x95\x08\xd7\x68\x92\xe3\x51\x81\x11\x0a\xe3\x12\x60\x20\xae\xbb\x18\xce\x1b\x86\x46\xc3\xed\xa5\xa3\x8b\xab\xd3\xab\xe3\xa0\x9c\xc4\x65\x9a\x56\xd9\xd4\x75\x83\xe7\xc4\xe1\x42\x39\xc1\x6b\x45\x03\xf2\xce\x4d\xa3\xb3\x58\x14\x48\x5e\xdd\xa2\x33\x6f\x23\xaa\x0b\x43\x06\xd0\x1d\x81\x26\xdf\x1d\x60\x3c\xcb\x2a\xfa\x1d\x57\xe1\x77\xac\xe3\x44\x1e\x9d\x7a\x2e\x7a\x61\xbc\x31\xa0\x45\x13\x20\x87\x45\x4e\x0b\x32\xf4\xce\x08\x52\x25\x36\x7b\xbb\xed\xe2\x4b\xf7\xea\xc3\x44\x1f\x96\xc6\x8d\xac\xe4\xad\x10\x51\xce\x94\xea\x5f\x6f\xdb\xd0\x58\x9b\xf3\x00\x09\x8d\x90\xfd\x5d\x4e\xc2\xb8\xee\xb3\xcd\x95\xc6\x0d\xb9\xba\xba\xeb\xe6\x1e\xe3\xae\xe6\xa4\x0c\xb9\xa3\xc5\xe9\xa4\xb5\x94\xde\x85\xbf\x8b\x6b\xc3\xab\xa2\xeb\xe7\xa3\xf7\x10\x88\x36\xa6\x2b\xdd\x32\xa5\xc9\x9e\x1e\xf0\x4d\xa2\xd2\xb2\x0c\x35\xc4\x25\xf3\x06\x11\x5f\xcb\x89\x98\x54\xef\x06\x7e\xaf\x56\x85\xaf\x1d\x61\x60\x08\x61\x50\x57\xa3\x6a\xc4\x28\x07\x7d\x18\x80\xc4\xf7\xd4\x34\xd7\x26\x73\x07\xe6\x0e\x1c\x7b\xa2\x59\x01\x61\x8e\xf0\x98\x1c\x44\x09\xd3\x81\x76\x23\xc3\x28\xa1\xf9\xae\x62\x54\xb7\x93\xd8\x3f\x6e\xeb\x13\x6d\x6c\x2d\x33\x7c\xa1\x59\x5f\xa2\x1b\x43\xc2\x51\xf5\xbc\x99\xd8\xf2\xb4\x9a\xfe\x50\x15\x09\x65\x01\x55\xa2\xc1\xd5\xf4\x44\x41\x94\xe6\xc0\x84\x42\xd7\x60\xd8\x13\xae\x21\xa9\x31\x89\xf7\x60\xeb\xfb\x98\xdd\x57\x22\x35\xcf\xd3\x97\x01\xfd\x6f\x52\x7e\xa2\xa9\x12\xd5\xd9\x29\xd4\x20\x2c\x27\x3b\x09\x3e\xb4\x65\x21\xb4\xc7\xd0\x3e\x87\x78\x7d\xf2\xd9\x37\xde\xb7\x1c\x73\xd7\xa7\x5e\xd3\xbc\xf0\x92\x45\x4c\x3d\x82\x37\x7b\xe8\x20\x7a\xfa\x08\xdf\xd4\x84\xbf\xbc\xf1\xd1\xce\x1b\xda\xff\x06\x00\x00\xff\xff\xf0\x1b\x32\x3e\xbc\x2c\x00\x00") 159 | 160 | func assetsSkeletonCssBytes() ([]byte, error) { 161 | return bindataRead( 162 | _assetsSkeletonCss, 163 | "assets/skeleton.css", 164 | ) 165 | } 166 | 167 | func assetsSkeletonCss() (*asset, error) { 168 | bytes, err := assetsSkeletonCssBytes() 169 | if err != nil { 170 | return nil, err 171 | } 172 | 173 | info := bindataFileInfo{name: "assets/skeleton.css", size: 11452, mode: os.FileMode(420), modTime: time.Unix(1445292578, 0)} 174 | a := &asset{bytes: bytes, info: info} 175 | return a, nil 176 | } 177 | 178 | var _assetsWarningPng = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x00\x75\x05\x8a\xfa\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\x00\x00\x05\x2c\x49\x44\x41\x54\x58\xc3\xed\x96\x5b\x6c\x14\x55\x18\xc7\x7f\xe7\xcc\xcc\xce\x5e\xd9\xdd\xde\xdb\x6d\x69\x0b\xc5\xb6\x5b\xa0\x50\x6e\x2a\x97\x22\xa2\x88\x88\xa8\xf1\x92\x2a\x89\xb7\x44\x7d\x30\x21\xa2\x89\x06\x4d\x34\xf2\xe2\x03\x46\x8d\xd1\xf0\x40\x34\xbe\xf8\x62\x34\x31\x1a\xaf\x51\x50\x5e\xbc\xa2\x89\x31\x40\x08\xa0\x44\x6d\x15\x6a\x0b\x74\x77\xbb\x33\x73\xe6\xf8\x30\xbb\xf4\x22\x18\xa8\xf4\x49\x4f\xf2\xe5\x9b\xcc\x9c\xef\xfb\xfe\xff\xff\x39\xdf\x39\x03\xff\xf5\x21\xa6\x1a\xf8\xe3\xca\xe4\x9a\xba\xd6\xba\x9d\x00\x03\x47\x07\x1e\xe8\xda\x7b\xf2\xd3\xa9\xe4\x91\x53\x09\xda\x98\xc0\x4e\x37\xd7\x6e\x8f\x5e\x7b\xfd\x9c\xe8\xfa\xeb\xe7\xa4\x9b\xeb\xb6\x6f\x4c\x60\x4f\x25\x97\x71\xa1\x01\xdb\xea\x4d\xeb\xe1\x59\x62\x6b\xed\x86\xb5\x77\x0b\x4b\x81\xca\x11\xce\x34\x34\x2d\xfb\xed\x60\xbe\x42\x5a\x5f\x7d\x76\xd2\x53\xd3\xaa\xc0\x35\x69\xd5\x55\xd5\xd3\xbe\x59\xd6\x54\x83\x1a\x05\x35\x8a\x4c\xc6\xa9\x5a\x36\x77\xf3\xd5\x49\x37\x3b\xad\x0a\xac\x89\x61\xde\xb5\xbc\xe1\x85\x54\xef\xea\xd5\x52\x8d\x20\xd0\x08\xa9\xc1\x2d\x60\xd5\xd4\x56\x87\x4e\x9f\xa8\x39\xf8\x87\x7a\xeb\x50\xce\xf5\xa7\x45\x81\xa7\x9b\x42\xbd\xa9\xae\xb6\x8d\x32\x6a\x81\xd0\x20\x75\xb0\x8d\xa5\x8f\x88\x9a\x24\xb3\x2d\xd7\x3d\x51\xc7\xea\x69\x51\xe0\xa3\xf9\xe1\x96\xec\xa2\xaa\x17\x63\x2b\xd7\xb4\xe2\x9c\x04\x4b\x83\xad\x03\x2f\x35\x5a\x39\x98\x0d\x2d\x46\x2c\x7f\xac\x6d\xbd\xd4\xbb\x5f\xeb\x77\x86\x2f\xa6\x02\xa2\x25\x13\xe9\x8b\xf5\x2c\x5d\x84\x33\x04\x96\x62\xcf\x20\xbc\xfc\x2d\xbc\xf4\x8d\x66\xcf\x71\x10\x96\x0f\xee\x10\xb1\x85\x3d\x3d\x33\x9b\xa2\x77\x9c\x6f\x8b\x9f\x17\x80\x77\xb3\xf1\xf6\x9a\x79\xb3\xb6\x99\xf5\x15\x61\xed\x17\xc1\xd4\xf4\x8f\x2a\xbe\x38\xe2\xf3\xe5\x51\xe8\x77\x34\x22\xa4\x41\x14\x31\x1a\x92\xe1\x9a\x79\x99\xc7\x3e\xbe\xb4\x2e\x7b\x51\x00\xec\xbc\x24\x11\x9f\xd7\xa2\x1f\xb2\xbb\xbb\xe2\xa8\x1c\x42\xfa\x80\x4f\xdc\xd6\x68\xad\xf1\x7d\x9f\xb8\xed\xa3\x85\x06\xc3\x07\x3d\x42\xa8\xbb\x3d\xd6\xd9\x54\xdc\xfa\xea\x92\xa6\xc4\xbf\x06\xb0\xaa\x59\xde\x9a\xee\xbd\xac\x4f\x98\x2e\x30\x0a\xd2\x07\xa9\x40\xf8\xf8\x7e\x60\x42\x6a\x84\xa1\x10\x86\x46\xc8\x22\xc2\x76\x48\xad\x5c\x70\xdb\x8a\x26\xa7\xef\x5f\x01\xd8\x54\x17\x8f\x54\xb5\x54\x6d\x09\x35\x56\x27\xf0\x46\x40\xf8\x60\x28\x90\x9a\xb0\x39\x06\xc0\x36\x15\x18\x1a\x4c\xbf\xa4\xc2\x69\xac\xe6\x74\xac\xb2\x39\xb9\xa5\xaf\xad\x2e\x32\x25\x00\xcf\x75\x56\xd8\xcf\x76\x5b\xdb\x12\xcb\xb2\xf3\x91\x79\x90\x1e\x18\x2a\x60\x8a\xc7\x8c\x90\x87\xe7\x79\x28\xa5\x98\x11\x2a\xa9\x62\x29\x08\xf9\x81\xb7\x73\xc4\x2e\x9b\x95\x7d\x66\x81\x78\x6a\xd7\xaa\xce\xf0\x05\x03\x58\x9e\x1a\xed\xae\xe8\x69\xed\x13\xa9\x28\xe8\x7c\x50\x5c\xaa\x60\x09\x0c\x1f\x4d\xc0\x5e\x29\x85\x16\x2a\x60\x6f\x96\xbc\xa1\x40\xe4\x11\x95\x36\xa9\xee\x8a\x5b\x97\xc6\x87\x16\x9e\xab\x8e\x79\xb6\x97\xf7\x35\x27\xed\x4c\x47\xc5\x93\x91\x8e\x99\xb3\xf5\xe8\x00\xc2\x10\x20\x25\xda\x34\x10\xa6\x0f\x12\x6c\x0b\xb4\xe7\xe2\x38\x45\x4c\x1c\x28\xe4\xc1\x2b\x80\x5b\x00\xd7\x01\xaf\x88\xf6\x14\xe1\xc6\x59\x2d\xf5\xb3\xed\xed\x8f\x2c\x9c\xbd\x61\xc7\x77\x87\x8b\xe7\x03\x40\xdc\x5f\x2d\xd6\x25\x3b\x32\x6b\x85\x55\x00\xed\x81\x94\x20\x75\xd0\x01\x5e\x1e\xbc\x1c\x21\x77\x98\xdf\x8e\xe6\x70\x5d\x07\x5b\xb9\x70\xdc\x03\xf4\x58\x16\x5d\x32\xf9\x07\xf1\xd6\x54\xef\xbd\x3f\xc9\x4d\x3b\xbe\xe3\x8d\x89\x93\xce\x72\x58\xec\x59\x9a\x98\xd3\xd5\x53\xf1\x7a\xbc\x77\xfe\x62\x8a\x03\x20\x5d\xc0\x41\x38\x7f\x82\x3b\x02\x6a\x04\x61\xc0\x70\x14\x76\x1f\x00\x69\xc0\x15\x73\x21\xe9\x4c\xca\x56\x02\xa0\x7d\x09\x91\x56\x0a\xfb\xf2\xdf\x1f\xda\x1f\xe9\x5b\xf2\xce\x91\x03\xff\xa4\x80\x68\xac\x0f\x6f\x8e\x74\xb4\xce\x63\xe4\x18\x14\x87\xc0\x19\x04\x1c\x10\x2e\x18\x20\xcc\x20\x2a\x9d\x82\x9b\x36\x82\x30\x80\x61\xd0\x7f\x4e\xca\xe4\x07\x00\x04\x3e\xb8\xbf\x10\x6e\x6b\xeb\xaa\x1f\x2e\xdc\x03\x3c\x3a\x5e\x85\x09\x0a\x7c\xb2\x20\xd9\xbd\xf8\xca\xea\x2f\x42\xcd\x56\x58\x9f\x3c\x04\xd2\x3b\x53\x70\x82\x8f\xc1\x7b\x3f\xc3\xce\x37\x83\x04\x0f\xde\x0e\xeb\xda\x83\x63\x62\x82\x02\x3e\x68\x1f\x50\x80\xd9\x80\xf7\x93\xe5\xfe\xb0\x37\xb2\x7c\xd9\x7b\x07\xbe\xfe\x5b\x17\xec\xea\x4c\x25\x2e\x99\xe9\x3d\x64\xd5\x87\xc3\x7a\xe8\x08\x68\x0f\x21\x82\x19\x67\xbc\x2c\x5d\x5f\x21\xe8\xcf\xc3\x87\xfb\xe0\x83\x7d\xc1\x33\xa1\xd2\x37\xb3\xe4\x4b\x73\xcf\xc4\xa8\x13\x18\x2d\x51\xab\x35\x33\xb8\xf5\xad\xf5\x3d\xc9\xbf\x01\x58\x91\x51\x7d\x33\xb2\x8d\x37\xeb\xfc\x31\xf0\x8b\x01\xb5\xb2\xc9\x89\x26\x44\x70\x1b\x8f\x97\x51\xc8\x40\x1d\x51\x5a\x26\x51\x06\x52\x06\x81\x83\xd0\xbf\x92\x98\x5f\x7d\xc3\xa2\xea\xe3\x77\x4e\xb8\x8e\xb7\xce\x4e\xc7\xaf\xea\xb4\x5f\x89\x34\x5a\x0d\x8c\x0e\x9c\x41\x2d\x4a\xc6\x64\x2f\xa1\x22\x03\x97\x2f\x87\x5b\x36\xc0\xe5\x5d\x90\x2a\xcb\x2e\xc7\x16\x56\x4c\xde\x13\xaa\x88\x8c\xcd\x30\x8d\x9c\xdf\x11\x51\xf5\xaf\xee\xf9\xe5\x44\xd1\x78\xbe\x35\x12\xbe\x3d\xa3\x1e\xaf\x5c\x52\x7b\x23\xce\xaf\x80\x1b\x14\x2e\xb3\x35\xcf\x02\x40\x40\x5a\x40\x36\x05\x5d\xa9\x52\x71\xc6\xb1\x3d\x4b\x37\x8c\x75\x45\x11\xab\xb2\x32\xdd\x3e\x98\x8b\x2e\xa8\x6a\xfc\xdc\xac\x31\x75\xc6\x8e\x79\xab\x29\xf4\x03\xf9\xa0\xe0\x85\xfe\xd8\x97\xac\x5c\x5c\xf8\xa0\xc5\xa4\xef\xe5\x67\x2f\x0f\xaa\x9f\x50\x94\x15\xd5\x46\x6c\xa6\xf9\x75\x4e\x7b\x83\xfb\x79\xfb\xd2\xd1\x53\x86\xb6\x91\x13\xd8\x8f\x93\x5c\x98\xe3\xde\x8d\xdf\x60\x72\x9c\x1f\xc7\x5c\xfb\x65\xc6\x63\x9d\x50\xf6\xa2\x70\xca\xff\xf1\x48\xdd\xfb\x87\xbd\x5c\x51\x00\x36\x50\x09\xd4\x9e\xeb\x68\x9e\x86\xe1\x01\xbf\x03\x83\xfc\x3f\xfe\xf3\xe3\x2f\xd2\x4e\xdb\x92\x2e\x8f\x68\x48\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\x01\x00\x00\xff\xff\x60\x80\xa7\x9d\x75\x05\x00\x00") 179 | 180 | func assetsWarningPngBytes() ([]byte, error) { 181 | return bindataRead( 182 | _assetsWarningPng, 183 | "assets/warning.png", 184 | ) 185 | } 186 | 187 | func assetsWarningPng() (*asset, error) { 188 | bytes, err := assetsWarningPngBytes() 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | info := bindataFileInfo{name: "assets/warning.png", size: 1397, mode: os.FileMode(420), modTime: time.Unix(1445292578, 0)} 194 | a := &asset{bytes: bytes, info: info} 195 | return a, nil 196 | } 197 | 198 | var _assetsWebsocketJs = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xe4\x56\xef\x6b\xe4\x36\x10\xfd\xbc\xfb\x57\xcc\x6d\x0f\x6c\x73\x7b\xde\xa3\x1f\xf7\x9a\x42\x8f\xb4\xe4\x4a\x93\x1c\x24\x50\xca\x71\x04\xad\x3d\x59\x2b\xb1\x25\x23\xc9\xd9\x98\xb2\xff\x7b\x67\x24\x79\xe3\xcd\x92\x4b\xfa\x0b\x0a\xcd\x87\xac\x65\x69\x9e\xde\x3c\xbd\x19\xf9\x4e\x18\xb0\xba\xb8\x85\x23\x50\xb8\x81\x5f\x71\x75\x41\x23\x74\xe9\xc6\x76\x46\x66\xef\xa7\x53\x9e\xcd\xb5\xd2\x2d\x2a\x5a\x74\xdd\xa9\xc2\x49\xad\xd2\x0c\x7e\x87\x42\x2b\xab\x6b\xcc\x6b\xbd\x4e\x67\x34\x50\x58\x38\x2c\xc1\x69\x98\xc1\x1b\x88\x08\xb0\x1d\x20\x8a\x5a\x5b\x1c\x63\xe0\x53\x20\x34\x09\x7e\x75\x09\x29\x43\x61\x5e\xe8\x12\xe9\x61\x96\xcd\xc6\x88\x0d\x5a\x2b\xd6\x07\x98\xd3\x09\xa7\x55\x54\xc2\xb8\x2b\x27\x1b\x9e\x7f\x9d\x26\xdf\xf0\xe3\xd5\x0a\x55\x51\x35\xc2\xdc\x26\x59\x5e\xc9\x75\xe5\x57\xd9\x94\x32\x1d\x05\x35\xd8\xc4\x18\x7a\xfa\x5a\xc8\x74\xd2\xd8\x35\x2d\xfd\xf9\xe2\xfc\x2c\x6f\x85\xb1\x98\x62\x5e\x0a\x27\x58\xb9\x89\xbc\x86\x94\xe6\x73\xd7\xb7\xc4\xe1\xe8\x08\x12\xeb\x84\xeb\x6c\xe2\x39\x4e\x68\x1d\x53\x6b\x74\x83\xca\xf9\x85\x85\x6e\x1a\xe9\x18\x00\xb3\xfc\x5a\x9b\x46\xb8\x34\xf9\x8d\xfe\xde\x9e\x9e\xbe\x3d\x3e\x86\x93\x93\x65\xd3\x2c\x2d\x01\xd0\xde\x13\x25\x7c\x6a\xf7\x77\xa2\xee\x70\x14\xef\x27\x4b\x5d\x74\x8c\x9b\xaf\xd1\xfd\x58\x23\x3f\x7e\xe8\x3f\x96\xe9\x8e\x42\x2e\x49\x6a\x73\x72\x79\xfa\x0b\x73\xa0\xe0\x30\xc1\xbc\x27\x8b\x05\x54\x92\x04\xaf\xb5\x28\xd1\x00\xe5\xa1\xb4\x83\x44\x2a\x68\x8d\x5e\x1b\x52\x3d\xa1\x55\x43\x7a\x21\x10\x5e\x71\x82\x1f\x47\x4b\x42\x96\xcf\x31\xb9\x92\x0d\x1d\x21\xf1\xb1\xae\x27\x1b\xdc\x49\x2b\x57\xb2\x96\xae\x27\x5a\x33\x62\x51\xa2\x9a\x71\x42\xdb\x81\x99\x28\x4b\xe0\xd3\x40\x63\x81\x7c\x82\xc6\x68\x33\x62\xe3\xc7\x9e\x4c\xa7\x4a\xbc\x96\x0a\xcb\x48\x84\x62\x2f\xcf\x8f\xcf\x97\xe0\x2a\x24\x23\x4a\x76\x1e\x99\xbf\xc1\x4a\x6f\xa0\xd4\x68\x55\xe2\x60\xa3\xcd\x2d\x6c\xa4\xab\x00\xef\x5b\x6d\xd8\xcf\x0f\x67\x9e\xdf\x58\x68\xd5\x7a\x71\xd3\xae\x23\x20\xe1\x97\xa0\x3b\x8a\xab\x7a\x48\x45\x6d\x50\x94\x3d\x58\x2a\x16\x07\xdf\x42\xa5\x3b\x63\x33\x10\xb4\xc6\x60\x5b\x8b\x02\x03\x34\x6f\x22\xd5\x9a\xe8\xa3\xcd\x19\x29\xe4\x43\x19\x27\x9d\xa9\xd3\x05\x2b\x23\x8b\xc5\x46\x18\x45\xcb\x72\xda\x32\x4b\x58\x83\xfd\x24\xf3\xcb\x9d\xaf\x5a\xa1\x64\x31\x08\xfe\x04\x9a\x5f\x33\xc2\xda\x4e\x3d\xa2\xf3\x5e\x0f\x81\xec\xa8\x25\xf0\xff\xb9\x1f\xf7\x4b\x78\x37\x1f\x41\x2e\xa9\x5e\x6d\xdf\xac\x74\xbd\x8c\x47\x40\xb5\x38\x19\xce\x66\xf2\x3a\x47\x51\x54\xe9\x43\xe1\xe5\x16\x8d\x44\x3b\x7f\x28\x4f\x92\x0b\xef\xe7\xe0\xdf\x0f\x74\xfd\x20\xa7\x63\xfd\xa4\x25\x15\x02\x33\x9a\x83\x33\x1d\x52\x9c\xa8\x2d\x66\x81\x6e\xf8\xd9\xdb\x83\xaa\xf3\x9f\xdf\x62\xfb\xfe\xd0\xdb\x2c\xf2\xec\x27\xa9\xa4\xad\xb0\x9c\x3d\x67\xec\x50\x86\x57\xab\x9a\xba\xd4\x0b\x8c\x0d\x48\x0c\x5e\x04\xf9\xa8\x6a\x7d\x03\x79\x03\x09\xa4\x09\xfd\x8c\x1a\x88\xed\x56\x37\xd4\x46\x79\x2e\xe3\xa9\xe4\xbb\x95\x81\xc5\xf7\xfe\xf1\x44\xd8\x6a\x09\x8f\x02\x2a\x7a\x39\xe4\x1e\xf9\x1c\x36\x2f\x2a\xea\xae\x76\xd1\x65\x61\x10\x5b\x47\x18\x78\x13\xc4\xf3\x09\x6f\xe8\x70\xdc\xe8\x64\x6e\xb1\x9f\x83\xef\x56\x51\x41\xee\xb8\xbe\xbb\x12\x8e\x7f\xff\xf9\xdd\x17\x7f\x12\xfb\xad\x31\x82\xfd\xb9\xee\xf8\xa8\x3d\xee\x61\x64\xc1\xaf\x54\xbf\xfe\x7a\xf0\x66\x62\x35\x05\xbf\x0d\x8e\xd9\xab\x8b\x83\xc2\xe0\xca\xf0\xc4\xf3\x33\xfb\x09\xcd\x79\x1b\x5e\xc7\x3a\x08\x76\x23\xd7\x8c\xee\x20\x3e\xd1\x34\x86\x10\x4c\xa0\xe8\x6d\x16\x16\xf3\x55\x48\x7c\x62\xe4\xd0\x48\xf0\x5e\x5a\x67\xc3\x96\x61\xea\x59\x13\xef\xfc\xc4\x70\xfe\x52\x0f\x81\x01\x64\x44\x88\x80\x2e\xfc\x4c\x1a\x93\x64\xd5\xc5\x12\x3e\x33\xf0\x97\x98\xe7\x44\x96\xbb\x4c\x1f\xb2\x8f\x7a\x1c\xbc\xdf\x0e\x14\x1e\xfa\x0b\x91\xa0\x2a\xd5\xa6\xff\x5b\x2a\xff\x50\x53\x2d\x61\xf9\xa1\x77\xf8\x02\xbd\xb9\x2d\xfc\x97\xe4\x66\x3e\xff\xbe\xda\xe1\x45\xb8\x23\xcf\xf4\x66\x0e\x84\x6c\xb8\x8e\xf4\x1d\xf5\xe9\x5b\xa5\x37\x0a\x86\x66\xc9\x37\x92\x54\x34\x72\xa0\xba\xba\xb6\x21\x8a\xe4\xf1\xc5\x42\x82\x18\x84\x46\x5a\xcb\xb7\xcf\xf4\xaf\x36\xf6\x6b\x4d\x37\x30\x7f\x9b\xb1\x5a\xe3\xe6\x1d\xba\x31\xe7\x3f\x06\x28\xef\x3d\xe5\x66\xe8\xd9\x7c\x58\x3c\xce\x43\x21\x1f\x79\x63\x0c\x93\x3b\x70\x3e\x90\x20\x03\xb5\x24\xd7\x19\x35\xda\x2e\x2a\x93\xed\xae\xce\x57\x3e\xea\x89\x4b\xe1\x2b\x26\x64\x8d\xa2\xe3\xf6\x2d\x10\x2f\xbf\xb1\xf4\x97\x15\x82\x65\xc6\xd4\xa1\xf6\xac\x3f\x1d\x7d\x81\x34\x74\x26\xfc\x9d\x6c\x91\x3e\x1a\x05\x7f\x64\x0c\x3a\x3c\x56\xfb\xa5\x57\xdc\xff\x4d\x6c\xda\x6e\x3b\xfd\x23\x00\x00\xff\xff\xeb\x23\x13\xc1\xbe\x0c\x00\x00") 199 | 200 | func assetsWebsocketJsBytes() ([]byte, error) { 201 | return bindataRead( 202 | _assetsWebsocketJs, 203 | "assets/websocket.js", 204 | ) 205 | } 206 | 207 | func assetsWebsocketJs() (*asset, error) { 208 | bytes, err := assetsWebsocketJsBytes() 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | info := bindataFileInfo{name: "assets/websocket.js", size: 3262, mode: os.FileMode(420), modTime: time.Unix(1445292578, 0)} 214 | a := &asset{bytes: bytes, info: info} 215 | return a, nil 216 | } 217 | 218 | // Asset loads and returns the asset for the given name. 219 | // It returns an error if the asset could not be found or 220 | // could not be loaded. 221 | func Asset(name string) ([]byte, error) { 222 | cannonicalName := strings.Replace(name, "\\", "/", -1) 223 | if f, ok := _bindata[cannonicalName]; ok { 224 | a, err := f() 225 | if err != nil { 226 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 227 | } 228 | return a.bytes, nil 229 | } 230 | return nil, fmt.Errorf("Asset %s not found", name) 231 | } 232 | 233 | // MustAsset is like Asset but panics when Asset would return an error. 234 | // It simplifies safe initialization of global variables. 235 | func MustAsset(name string) []byte { 236 | a, err := Asset(name) 237 | if (err != nil) { 238 | panic("asset: Asset(" + name + "): " + err.Error()) 239 | } 240 | 241 | return a 242 | } 243 | 244 | // AssetInfo loads and returns the asset info for the given name. 245 | // It returns an error if the asset could not be found or 246 | // could not be loaded. 247 | func AssetInfo(name string) (os.FileInfo, error) { 248 | cannonicalName := strings.Replace(name, "\\", "/", -1) 249 | if f, ok := _bindata[cannonicalName]; ok { 250 | a, err := f() 251 | if err != nil { 252 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 253 | } 254 | return a.info, nil 255 | } 256 | return nil, fmt.Errorf("AssetInfo %s not found", name) 257 | } 258 | 259 | // AssetNames returns the names of the assets. 260 | func AssetNames() []string { 261 | names := make([]string, 0, len(_bindata)) 262 | for name := range _bindata { 263 | names = append(names, name) 264 | } 265 | return names 266 | } 267 | 268 | // _bindata is a table, holding each asset generator, mapped to its name. 269 | var _bindata = map[string]func() (*asset, error){ 270 | "assets/index.html": assetsIndexHtml, 271 | "assets/loader.gif": assetsLoaderGif, 272 | "assets/main.css": assetsMainCss, 273 | "assets/panic.png": assetsPanicPng, 274 | "assets/skeleton.css": assetsSkeletonCss, 275 | "assets/warning.png": assetsWarningPng, 276 | "assets/websocket.js": assetsWebsocketJs, 277 | } 278 | 279 | // AssetDir returns the file names below a certain 280 | // directory embedded in the file by go-bindata. 281 | // For example if you run go-bindata on data/... and data contains the 282 | // following hierarchy: 283 | // data/ 284 | // foo.txt 285 | // img/ 286 | // a.png 287 | // b.png 288 | // then AssetDir("data") would return []string{"foo.txt", "img"} 289 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 290 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 291 | // AssetDir("") will return []string{"data"}. 292 | func AssetDir(name string) ([]string, error) { 293 | node := _bintree 294 | if len(name) != 0 { 295 | cannonicalName := strings.Replace(name, "\\", "/", -1) 296 | pathList := strings.Split(cannonicalName, "/") 297 | for _, p := range pathList { 298 | node = node.Children[p] 299 | if node == nil { 300 | return nil, fmt.Errorf("Asset %s not found", name) 301 | } 302 | } 303 | } 304 | if node.Func != nil { 305 | return nil, fmt.Errorf("Asset %s not found", name) 306 | } 307 | rv := make([]string, 0, len(node.Children)) 308 | for childName := range node.Children { 309 | rv = append(rv, childName) 310 | } 311 | return rv, nil 312 | } 313 | 314 | type bintree struct { 315 | Func func() (*asset, error) 316 | Children map[string]*bintree 317 | } 318 | var _bintree = &bintree{nil, map[string]*bintree{ 319 | "assets": &bintree{nil, map[string]*bintree{ 320 | "index.html": &bintree{assetsIndexHtml, map[string]*bintree{ 321 | }}, 322 | "loader.gif": &bintree{assetsLoaderGif, map[string]*bintree{ 323 | }}, 324 | "main.css": &bintree{assetsMainCss, map[string]*bintree{ 325 | }}, 326 | "panic.png": &bintree{assetsPanicPng, map[string]*bintree{ 327 | }}, 328 | "skeleton.css": &bintree{assetsSkeletonCss, map[string]*bintree{ 329 | }}, 330 | "warning.png": &bintree{assetsWarningPng, map[string]*bintree{ 331 | }}, 332 | "websocket.js": &bintree{assetsWebsocketJs, map[string]*bintree{ 333 | }}, 334 | }}, 335 | }} 336 | 337 | // RestoreAsset restores an asset under the given directory 338 | func RestoreAsset(dir, name string) error { 339 | data, err := Asset(name) 340 | if err != nil { 341 | return err 342 | } 343 | info, err := AssetInfo(name) 344 | if err != nil { 345 | return err 346 | } 347 | err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) 348 | if err != nil { 349 | return err 350 | } 351 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 352 | if err != nil { 353 | return err 354 | } 355 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 356 | if err != nil { 357 | return err 358 | } 359 | return nil 360 | } 361 | 362 | // RestoreAssets restores an asset under the given directory recursively 363 | func RestoreAssets(dir, name string) error { 364 | children, err := AssetDir(name) 365 | // File 366 | if err != nil { 367 | return RestoreAsset(dir, name) 368 | } 369 | // Dir 370 | for _, child := range children { 371 | err = RestoreAssets(dir, filepath.Join(name, child)) 372 | if err != nil { 373 | return err 374 | } 375 | } 376 | return nil 377 | } 378 | 379 | func _filePath(dir, name string) string { 380 | cannonicalName := strings.Replace(name, "\\", "/", -1) 381 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 382 | } 383 | 384 | 385 | func assetFS() *assetfs.AssetFS { 386 | for k := range _bintree.Children { 387 | return &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: k} 388 | } 389 | panic("unreachable") 390 | } 391 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | // RunError represents command running error 11 | type RunError struct { 12 | Message string 13 | Stderr string 14 | Type RunErrorType 15 | } 16 | 17 | // RunErrorType is a subtype for run error. 18 | type RunErrorType string 19 | 20 | // Errors messages. 21 | const ( 22 | PanicErr RunErrorType = "panic" 23 | BuildFailedErr = "build_failed" 24 | NoBenchmarksErr = "no_benchmarks" 25 | OtherErr = "other" 26 | ) 27 | 28 | // Run launches command in the given dir and handles success/errors. 29 | func Run(dir, command string, args ...string) (string, error) { 30 | cmd := exec.Command(command, args...) 31 | cmd.Dir = dir 32 | 33 | var stderr, stdout bytes.Buffer 34 | cmd.Stderr = &stderr 35 | cmd.Stdout = &stdout 36 | 37 | err := cmd.Run() 38 | if err != nil { 39 | return "", &RunError{ 40 | Type: guessErrType(err, stderr.String()), 41 | Message: err.Error(), 42 | Stderr: stderr.String(), 43 | } 44 | } 45 | 46 | return stdout.String(), nil 47 | } 48 | 49 | // guessErrType tries to guess error type based on stderr and other info. 50 | func guessErrType(err error, stderr string) RunErrorType { 51 | lines := strings.Split(stderr, "\n") 52 | if len(lines) > 2 { 53 | if strings.HasPrefix(lines[0], "panic:") || strings.HasPrefix(lines[1], "panic:") { 54 | return PanicErr 55 | } 56 | if strings.HasPrefix(lines[0], "# ") || strings.HasPrefix(lines[1], "# ") || strings.HasPrefix(lines[0], "can't load package") { 57 | return BuildFailedErr 58 | } 59 | } 60 | return OtherErr 61 | } 62 | 63 | // Error implements error interface for RunError. 64 | func (r *RunError) Error() string { 65 | if r.Stderr != "" { 66 | return fmt.Sprintf("failed: %s", r.Stderr) 67 | } 68 | 69 | return fmt.Sprintf("failed: %s", r.Message) 70 | } 71 | -------------------------------------------------------------------------------- /cmd_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestErrorGuess(t *testing.T) { 9 | Convey("Error guessing should guess error types correctly", t, func() { 10 | Convey("Panic", func() { 11 | typ := guessErrType(nil, panicStderr1) 12 | So(typ, ShouldEqual, PanicErr) 13 | typ = guessErrType(nil, panicStderr2) 14 | So(typ, ShouldEqual, PanicErr) 15 | }) 16 | }) 17 | } 18 | 19 | var panicStderr1 = `panic: nil 20 | 21 | goroutine 33 [running]: 22 | pkg.panicWrapper() 23 | /var/folders/qp/6bvmky410dn8p1yhn3b19yxr0000gn/T/gobenchui386989384/src/pkg/main.go:106 +0x23 24 | pkg.BenchmarkStrconvConcat(0x820362100) 25 | /var/folders/qp/6bvmky410dn8p1yhn3b19yxr0000gn/T/gobenchui386989384/src/pkg/main_test.go:19 +0x214 26 | testing.(*B).runN(0x820362100, 0x1) 27 | /usr/local/go/src/testing/benchmark.go:124 +0x9a 28 | testing.(*B).launch(0x820362100) 29 | /usr/local/go/src/testing/benchmark.go:199 +0x63 30 | created by testing.(*B).run 31 | /usr/local/go/src/testing/benchmark.go:179 +0x54 32 | 33 | goroutine 1 [runnable]: 34 | testing.(*B).run(0x820362100, 0x0, 0x0, 0x0, 0x0, 0x0) 35 | /usr/local/go/src/testing/benchmark.go:180 +0x7b 36 | testing.RunBenchmarks(0x1cacc0, 0x2527a0, 0x3, 0x3) 37 | /usr/local/go/src/testing/benchmark.go:332 +0x75f 38 | testing.(*M).Run(0x8202ebef8, 0x8202ba4f0) 39 | /usr/local/go/src/testing/testing.go:503 +0x1b8 40 | main.main() 41 | pkg/_test/_testmain.go:58 +0x116` 42 | 43 | var panicStderr2 = `testing: warning: no tests to run 44 | panic: nil 45 | 46 | goroutine 33 [running]: 47 | pkg.panicWrapper() 48 | /var/folders/qp/6bvmky410dn8p1yhn3b19yxr0000gn/T/gobenchui386989384/src/pkg/main.go:106 +0x23 49 | pkg.BenchmarkStrconvConcat(0x820362100) 50 | /var/folders/qp/6bvmky410dn8p1yhn3b19yxr0000gn/T/gobenchui386989384/src/pkg/main_test.go:19 +0x214 51 | testing.(*B).runN(0x820362100, 0x1)` 52 | -------------------------------------------------------------------------------- /demo/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divan/gobenchui/a6b1cf870779eec5def3794b05b1f7c78e0d412a/demo/demo.png -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // FilterOptions represents advanced filtering for vcs commits. 8 | type FilterOptions struct { 9 | // LastN is for 'last N commits' 10 | // Default: 0 (disabled) 11 | LastN int64 12 | // Max defines how many maximum commits should be taken. 13 | // If total commits number is bigger than Max, 14 | // it tries to pick commits evenly, so benchmark result 15 | // can be representable in overall. 16 | // Default: 100 (disabled) 17 | Max int64 18 | 19 | Args []string 20 | } 21 | 22 | // NewFilterOptions creates new FilterOptions. 23 | func NewFilterOptions(lastN, max int64, args ...string) *FilterOptions { 24 | return &FilterOptions{ 25 | LastN: lastN, 26 | Max: max, 27 | Args: args, 28 | } 29 | } 30 | 31 | // FilterMax filters commits, selecting at most 'max' 32 | // evenly distributed items. 33 | // 34 | // Idea is to get brief overview of benchmark progress 35 | // overtime for codebases with large number of commits. 36 | // If there are 10K commits and max=100, it will pick 37 | // 100 commits from very beginning to the last one, with 38 | // the equal time intervals between each picked commit. 39 | func FilterMax(commits []Commit, max int64) []Commit { 40 | // special cases 41 | if max < 2 { 42 | return commits 43 | } 44 | length := int64(len(commits)) 45 | if max >= length || length == 0 { 46 | return commits 47 | } 48 | 49 | // rough naive implementation (by commits number) 50 | var ret []Commit 51 | for i := int64(0); i < length; { 52 | // how many items left in target slice 53 | left := max - int64(len(ret)) - 1 54 | if left == 0 { 55 | left = 1 56 | } 57 | // size of next step 58 | size := (length - i - 1) / left 59 | if size == 0 { 60 | size = 1 61 | } 62 | 63 | ret = append(ret, commits[i]) 64 | i = i + size 65 | } 66 | return ret 67 | } 68 | 69 | // String implements Stringer for FilterOptions. 70 | func (f *FilterOptions) String() string { 71 | out := "" 72 | if f.Max > 0 { 73 | out = fmt.Sprintf("%smax %d from ", out, f.Max) 74 | } 75 | if f.LastN > 0 { 76 | out = fmt.Sprintf("%slast %d ", out, f.LastN) 77 | } else { 78 | out = fmt.Sprintf("%sall ", out) 79 | } 80 | out = fmt.Sprintf("%scommits", out) 81 | return out 82 | } 83 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | func TestFilterMax(t *testing.T) { 11 | Convey("Filter Max should filter commits evenly", t, func() { 12 | N := int64(100) 13 | var commits []Commit 14 | for i := int64(0); i < N; i++ { 15 | date, _ := time.Parse("2006-01-02 15:04:05", fmt.Sprintf("2015-%02d-%02d 18:%02d:01", i%12+1, i%28+1, i%60)) 16 | commit := Commit{ 17 | Hash: fmt.Sprintf("Hash %d", i), 18 | Author: fmt.Sprintf("Author %d", i%3), 19 | Subject: fmt.Sprintf("Subject %d", i), 20 | Date: date, 21 | } 22 | commits = append(commits, commit) 23 | } 24 | Convey("Special case, len == 0", func() { 25 | res := FilterMax([]Commit{}, 0) 26 | So(len(res), ShouldEqual, 0) 27 | }) 28 | Convey("Special case, max > len", func() { 29 | res := FilterMax(commits, N*2) 30 | So(len(res), ShouldEqual, N) 31 | }) 32 | Convey("Special case, max == 0", func() { 33 | res := FilterMax(commits, 0) 34 | So(len(res), ShouldEqual, N) 35 | }) 36 | Convey("Max = 2, start and end", func() { 37 | res := FilterMax(commits, 2) 38 | So(len(res), ShouldEqual, 2) 39 | So(res[0].Hash, ShouldEqual, "Hash 0") 40 | So(res[1].Hash, ShouldEqual, "Hash 99") 41 | }) 42 | Convey("Max = 3, start, middle and end", func() { 43 | res := FilterMax(commits, 3) 44 | So(len(res), ShouldEqual, 3) 45 | So(res[0].Hash, ShouldEqual, "Hash 0") 46 | So(res[1].Hash, ShouldEqual, "Hash 49") 47 | So(res[2].Hash, ShouldEqual, "Hash 99") 48 | }) 49 | Convey("Max = 10, tens", func() { 50 | res := FilterMax(commits, 10) 51 | So(len(res), ShouldEqual, 10) 52 | So(res[0].Hash, ShouldEqual, "Hash 0") 53 | So(res[1].Hash, ShouldEqual, "Hash 11") 54 | So(res[2].Hash, ShouldEqual, "Hash 22") 55 | So(res[3].Hash, ShouldEqual, "Hash 33") 56 | So(res[4].Hash, ShouldEqual, "Hash 44") 57 | So(res[5].Hash, ShouldEqual, "Hash 55") 58 | So(res[6].Hash, ShouldEqual, "Hash 66") 59 | So(res[7].Hash, ShouldEqual, "Hash 77") 60 | So(res[8].Hash, ShouldEqual, "Hash 88") 61 | So(res[9].Hash, ShouldEqual, "Hash 99") 62 | }) 63 | Convey("Max = 98, near len", func() { 64 | res := FilterMax(commits, 98) 65 | So(len(res), ShouldEqual, 98) 66 | So(res[0].Hash, ShouldEqual, "Hash 0") 67 | So(res[90].Hash, ShouldEqual, "Hash 90") 68 | So(res[96].Hash, ShouldEqual, "Hash 97") 69 | So(res[97].Hash, ShouldEqual, "Hash 99") 70 | }) 71 | Convey("Max = 75, 3/4", func() { 72 | res := FilterMax(commits, 75) 73 | So(len(res), ShouldEqual, 75) 74 | So(res[0].Hash, ShouldEqual, "Hash 0") 75 | So(res[1].Hash, ShouldEqual, "Hash 1") 76 | So(res[73].Hash, ShouldEqual, "Hash 97") 77 | So(res[74].Hash, ShouldEqual, "Hash 99") 78 | }) 79 | }) 80 | Convey("Filter string() should generate text correctly", t, func() { 81 | Convey("When nothing specified", func() { 82 | str := NewFilterOptions(0, 0).String() 83 | So(str, ShouldEqual, "all commits") 84 | }) 85 | Convey("When lastN and Max specified", func() { 86 | str := NewFilterOptions(99, 20).String() 87 | So(str, ShouldEqual, "max 20 from last 99 commits") 88 | }) 89 | Convey("When lastN specified", func() { 90 | str := NewFilterOptions(150, 0).String() 91 | So(str, ShouldEqual, "last 150 commits") 92 | }) 93 | Convey("When Max specified", func() { 94 | str := NewFilterOptions(0, 25).String() 95 | So(str, ShouldEqual, "max 25 from all commits") 96 | }) 97 | }) 98 | } 99 | -------------------------------------------------------------------------------- /gopath.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | // GOPATH extracts first gopath from your env variable GOPATH. 10 | func GOPATH() string { 11 | gopath := os.Getenv("GOPATH") 12 | if gopath == "" { 13 | fmt.Fprintf(os.Stderr, "GOPATH not set, aborting") 14 | os.Exit(1) 15 | } 16 | paths := strings.Split(gopath, string(os.PathListSeparator)) 17 | return paths[0] 18 | } 19 | -------------------------------------------------------------------------------- /highcharts.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // HighchartsData holds series data in format 8 | // compatible with highcharts js. 9 | // To be used with html templates. 10 | type HighchartsData struct { 11 | Categories []Commit `json:"categories,omitempty"` 12 | Series []*Serie `json:"series,omitempty"` 13 | } 14 | 15 | // Serie is a single serie object. 16 | // Name should be the name of benchmark. 17 | type Serie struct { 18 | ID string `json:"id"` 19 | Name string `json:"name"` 20 | Data []*Point `json:"data"` 21 | } 22 | 23 | // Point represents single data point in highchart.js. 24 | // Name should be the name/id of commit. 25 | type Point struct { 26 | Name string `json:"name"` 27 | Value *float64 `json:"y"` 28 | Marker *Marker `json:"marker,omitempty"` 29 | } 30 | 31 | // Marker is an icon marker for points. 32 | type Marker struct { 33 | Symbol string `json:"symbol,omitempty"` 34 | } 35 | 36 | // xvalue mirrors xvalue() func in js code, 37 | // it formats commit to serve as an X value for 38 | // charts 39 | func xvalue(commit Commit) string { 40 | date := commit.Date.Format("06-01-02") 41 | var hash string 42 | if len(commit.Hash) > 6 { 43 | hash = commit.Hash[0:6] 44 | } 45 | return fmt.Sprintf("%s/%s", date, hash) 46 | } 47 | 48 | // AddResult adds and converts benchmark set into 49 | // highcharts-compatible representation of series/points. 50 | // 51 | // typ defines which result goes to this serie: "time" or "memory" 52 | func (d *HighchartsData) AddResult(b BenchmarkSet, typ string) { 53 | pointName := xvalue(b.Commit) 54 | 55 | findSerie := func(name string) *Serie { 56 | if d.Series == nil { 57 | return nil 58 | } 59 | for _, s := range d.Series { 60 | if s.Name == name { 61 | return s 62 | } 63 | } 64 | return nil 65 | } 66 | 67 | // Add error values for all series on error 68 | if b.Error != nil { 69 | for _, serie := range d.Series { 70 | value := 0.0 71 | 72 | // choose different icons for build error and panic error 73 | symbol := "url(/static/warning.png)" 74 | if er, ok := b.Error.(*RunError); ok { 75 | if er.Type == PanicErr { 76 | symbol = "url(/static/panic.png)" 77 | } 78 | } 79 | point := &Point{ 80 | Name: pointName, 81 | Value: &value, 82 | Marker: &Marker{ 83 | Symbol: symbol, 84 | }, 85 | } 86 | serie.Data = append(serie.Data, point) 87 | } 88 | return 89 | } 90 | 91 | for name, bench := range b.Set { 92 | serie := findSerie(name) 93 | if serie == nil { 94 | serie = &Serie{ID: name, Name: name} 95 | d.Series = append(d.Series, serie) 96 | } 97 | 98 | point := &Point{ 99 | Name: pointName, 100 | Value: nil, 101 | } 102 | switch typ { 103 | case "time": 104 | point.Value = &(bench[0].NsPerOp) 105 | case "memory": 106 | val := float64(bench[0].AllocedBytesPerOp) 107 | point.Value = &val 108 | } 109 | serie.Data = append(serie.Data, point) 110 | } 111 | 112 | // Now, iterate over series and add null values 113 | // for this commit if no benchmarks were conducted. 114 | for _, serie := range d.Series { 115 | var found bool 116 | for name := range b.Set { 117 | if name == serie.Name { 118 | found = true 119 | break 120 | } 121 | } 122 | 123 | if !found { 124 | point := &Point{ 125 | Name: pointName, 126 | Value: nil, 127 | } 128 | serie.Data = append(serie.Data, point) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /info.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Status represents a benchmark status. 9 | type Status string 10 | 11 | // Process statuses. 12 | const ( 13 | Starting Status = "Starting" 14 | InProgress = "In progress" 15 | Finished = "Finished" 16 | Aborted = "Aborted" 17 | Failed = "Failed" 18 | ) 19 | 20 | // BenchmarkStatus holds details of current status. 21 | type BenchmarkStatus struct { 22 | Status Status `json:"status"` 23 | Progress float64 `json:"progress"` 24 | CurrentCommit *Commit `json:"commit,omitempty"` 25 | } 26 | 27 | // Info holds information about bench session, 28 | // like pkg name, start time, progress, status, etc. 29 | type Info struct { 30 | mx *sync.RWMutex 31 | 32 | BenchmarkStatus 33 | 34 | PkgName string `json:"pkg_name"` 35 | PkgPath string `json:"pkg_path"` 36 | VCS string `json:"vcs"` 37 | 38 | BenchOptions string `json:"bench_options"` 39 | Filter string `json:"filter"` 40 | Commits []Commit `json:"commits"` 41 | 42 | BenchResults []BenchmarkSet `json:"results"` 43 | TimeSeries *HighchartsData `json:"time_series,omitempty"` 44 | MemSeries *HighchartsData `json:"mem_series,omitempty"` 45 | 46 | StartTime time.Time `json:"start_time"` 47 | EndTime time.Time `json:"end_time"` 48 | } 49 | 50 | // NewInfo returns new initialized info. 51 | func NewInfo(pkg, path, vcs, benchopts string, filter *FilterOptions, commits []Commit) *Info { 52 | return &Info{ 53 | mx: &sync.RWMutex{}, 54 | 55 | BenchmarkStatus: BenchmarkStatus{ 56 | Status: Starting, 57 | Progress: 0.0, 58 | }, 59 | 60 | PkgName: pkg, 61 | PkgPath: path, 62 | VCS: vcs, 63 | 64 | BenchOptions: benchopts, 65 | Filter: filter.String(), 66 | Commits: commits, 67 | 68 | TimeSeries: &HighchartsData{Categories: commits}, 69 | MemSeries: &HighchartsData{Categories: commits}, 70 | 71 | StartTime: time.Now(), 72 | } 73 | } 74 | 75 | // SetProgress is a setter for Progress value. 76 | func (i *Info) SetProgress(v float64) { 77 | i.mx.Lock() 78 | i.Progress = v 79 | i.mx.Unlock() 80 | } 81 | 82 | // SetCommit is a setter for Current Commit value. 83 | func (i *Info) SetCommit(commit *Commit) { 84 | i.mx.Lock() 85 | i.CurrentCommit = commit 86 | i.mx.Unlock() 87 | } 88 | 89 | // SetStatus changes status of execution. 90 | func (i *Info) SetStatus(status Status) { 91 | i.mx.Lock() 92 | defer i.mx.Unlock() 93 | i.Status = status 94 | if status == Finished { 95 | i.EndTime = time.Now() 96 | } 97 | } 98 | 99 | // AddResult inserts new BenchmarkSet result to Info. 100 | func (i *Info) AddResult(b BenchmarkSet) { 101 | i.mx.Lock() 102 | defer i.mx.Unlock() 103 | 104 | i.BenchResults = append(i.BenchResults, b) 105 | i.TimeSeries.AddResult(b, "time") 106 | i.MemSeries.AddResult(b, "memory") 107 | } 108 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | ) 9 | 10 | var ( 11 | // ProgramName specifies default program name 12 | // (for tempfiles, etc) 13 | ProgramName = "gobenchui" 14 | 15 | bind = flag.String("bind", ":6222", "host:port to bind http server to") 16 | vcsArgs = flag.String("vcsArgs", "", "Additional args for vcs command (git, hg, etc)") 17 | benchArgs = flag.String("bench", ".", "Regexp for benchmarks, as for `go test -bench`") 18 | lastN = flag.Int64("last", 0, "Last N commits only") 19 | max = flag.Int64("max", 0, "Maximum commits (distribute evenly)") 20 | ) 21 | 22 | func main() { 23 | flag.Parse() 24 | if len(flag.Args()) != 1 { 25 | Usage() 26 | os.Exit(1) 27 | } 28 | 29 | pkg := flag.Arg(0) 30 | gopath := GOPATH() 31 | path, err := absPath(pkg, gopath) 32 | if err != nil { 33 | fmt.Fprintln(os.Stderr, "Failed to find package:", err) 34 | os.Exit(1) 35 | } 36 | pkg = normalizePkgName(pkg, path, gopath) 37 | fmt.Println("[INFO] Benchmarking package", pkg) 38 | 39 | var vcs VCS 40 | filter := NewFilterOptions(*lastN, *max, *vcsArgs) 41 | 42 | vcs, err = NewGitVCS(path, *filter) 43 | if err != nil { 44 | vcs, err = NewHgVCS(path, *filter) 45 | if err != nil { 46 | fmt.Fprintln(os.Stderr, "package isn't under any supported VCS, so no benchmarks to compare") 47 | os.Exit(1) 48 | } 49 | } 50 | 51 | err = vcs.Workspace().Clone() 52 | if err != nil { 53 | fmt.Fprintln(os.Stderr, "Couldn't clone dir:", err) 54 | os.Exit(1) 55 | } 56 | 57 | // Remove temporary directory in the end 58 | cleanup := func() { 59 | path := vcs.Workspace().Gopath() 60 | os.RemoveAll(path) 61 | } 62 | defer cleanup() 63 | 64 | // Prepare commits to run benchmarks agains 65 | commits, err := vcs.Commits() 66 | if err != nil { 67 | fmt.Fprintln(os.Stderr, "Couldn't get commits:", err) 68 | return 69 | } 70 | 71 | ch := RunBenchmarks(vcs, commits, *benchArgs) 72 | 73 | info := NewInfo(pkg, path, vcs.Name(), *benchArgs, filter, commits) 74 | info.SetStatus(InProgress) 75 | 76 | // There is basically no reason to make this channel 77 | // buffered, but just in case, if web frontend code will 78 | // stuck (websocket js issue or smth.), results will 79 | // still be saved into info, so the page reload will 80 | // show all results. 81 | webCh := make(chan interface{}, 1024) 82 | go func() { 83 | for { 84 | select { 85 | case val, ok := <-ch: 86 | if !ok { 87 | info.SetStatus(Finished) 88 | info.SetCommit(nil) 89 | webCh <- BenchmarkStatus{ 90 | Status: Finished, 91 | Progress: 100.0, 92 | } 93 | return 94 | } 95 | if result, ok := val.(BenchmarkSet); ok { 96 | info.AddResult(result) 97 | info.SetStatus(InProgress) 98 | } 99 | if status, ok := val.(BenchmarkRun); ok { 100 | // On error, insert error marker instead result 101 | if status.Error != nil { 102 | res := BenchmarkSet{ 103 | Commit: status.Commit, 104 | Error: status.Error, 105 | } 106 | info.AddResult(res) 107 | } 108 | info.SetStatus(InProgress) 109 | info.SetCommit(&status.Commit) 110 | } 111 | 112 | webCh <- val 113 | } 114 | } 115 | }() 116 | 117 | go StartServer(*bind, webCh, info) 118 | 119 | // don't exit, even after all benchmarks had been completed, 120 | // as we need to keep serve web page 121 | sigCh := make(chan os.Signal, 1) 122 | signal.Notify(sigCh, os.Interrupt, os.Kill) 123 | <-sigCh 124 | fmt.Println("Got signal, exiting...") 125 | } 126 | 127 | // Usage prints program usage text. 128 | func Usage() { 129 | fmt.Fprintf(os.Stderr, "Usage: %s package\n", os.Args[0]) 130 | flag.PrintDefaults() 131 | } 132 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | // absPath returns absolute path to package to be benchmarked. 10 | // For package names it looks for them in GOPATH. 11 | // For '.' it resolves current working directory. 12 | func absPath(pkg, gopath string) (string, error) { 13 | if pkg == "." { 14 | return os.Getwd() 15 | } 16 | 17 | path := filepath.Join(gopath, "src", pkg) 18 | return filepath.Clean(path), nil 19 | } 20 | 21 | // normalizePkgName guesses correct package name 22 | // to be shown on UI. 23 | // Usually package is referenced by proper name, 24 | // but when using dot directory, we need to guess it. 25 | func normalizePkgName(pkg, absPath, gopath string) string { 26 | if pkg != "." { 27 | return pkg 28 | } 29 | 30 | prefix := filepath.Join(gopath, "src") + string(filepath.Separator) 31 | if strings.HasPrefix(absPath, prefix) { 32 | return strings.TrimPrefix(absPath, prefix) 33 | } 34 | 35 | // else, it means project is in it's own directory, 36 | // outside of GOPATH scope, so just use it's basename 37 | return filepath.Base(absPath) 38 | } 39 | 40 | // findPrefix find relative dir in top-level dir. 41 | // It's basically the same as `git rev-parse --show-prefix`. 42 | // 43 | // If package is in top-level directory, prefix is "". 44 | func findPrefix(path, root string) string { 45 | prefix := strings.TrimPrefix(path, root) 46 | prefix = strings.TrimPrefix(prefix, "/") 47 | return prefix 48 | } 49 | -------------------------------------------------------------------------------- /path_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestPathHelpers(t *testing.T) { 9 | Convey("Path helpers should work correctly", t, func() { 10 | Convey("Abs path should handle GOPATH", func() { 11 | path, err := absPath("github.com/coreos/etcd", "/home/user/gopath") 12 | So(err, ShouldBeNil) 13 | So(path, ShouldEqual, "/home/user/gopath/src/github.com/coreos/etcd") 14 | }) 15 | Convey("NormalizePkgName should normalize names correctly", func() { 16 | path := "/home/user/gopath/src/github.com/coreos/etcd" 17 | name := normalizePkgName(".", path, "/home/user/gopath") 18 | So(name, ShouldEqual, "github.com/coreos/etcd") 19 | }) 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /vcs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // VCS represents our needs from VCS - list commits, switch 8 | // to specific commit and obtain previous commit. 9 | type VCS interface { 10 | Commits() ([]Commit, error) 11 | SwitchTo(hash string) error 12 | Workspace() *Workspace 13 | Name() string 14 | } 15 | 16 | // Commit represents single commit in VCS. 17 | // 18 | // Author is the last person touched this commit 19 | // (committer in git terms) 20 | type Commit struct { 21 | Hash string `json:"hash"` 22 | Author string `json:"author"` 23 | Subject string `json:"subject"` 24 | Date time.Time `json:"date"` 25 | } 26 | -------------------------------------------------------------------------------- /vcs_git.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Git implements VCS for Git version control. 12 | type Git struct { 13 | workspace *Workspace 14 | 15 | filter FilterOptions 16 | } 17 | 18 | // NewGitVCS returns new Git vcs, and checks it it's valid git workspace. 19 | func NewGitVCS(path string, filter FilterOptions) (*Git, error) { 20 | // check if this path is under git control 21 | _, err := Run(path, "git", "rev-parse", "--git-dir") 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | // find top-level (root) directory of workspace 27 | out, err := Run(path, "git", "rev-parse", "--show-toplevel") 28 | if err != nil { 29 | return nil, errors.New("cannot determine root folder") 30 | } 31 | root := strings.TrimSpace(out) 32 | prefix := findPrefix(path, root) 33 | 34 | workspace := NewWorkspace(root, prefix) 35 | vcs := &Git{ 36 | workspace: workspace, 37 | filter: filter, 38 | } 39 | return vcs, nil 40 | } 41 | 42 | // Commits returns all commits for the current branch. Implements VCS interface. 43 | func (g *Git) Commits() ([]Commit, error) { 44 | path := g.Workspace().Path() 45 | 46 | // Prepare args, and add user defined args to `git log` command 47 | args := []string{"log", `--pretty=format:%H|%cd|%cn <%ce>|%s`, `--date=rfc`} 48 | if len(g.filter.Args) > 0 { 49 | // Append custom arguments, excluding formatting-related ones 50 | cleanedArgs := cleanGitArgs(g.filter.Args...) 51 | args = append(args, cleanedArgs...) 52 | } 53 | 54 | if g.filter.LastN > 0 { 55 | lastNArg := fmt.Sprintf("-n %d", g.filter.LastN) 56 | args = append(args, lastNArg) 57 | } 58 | 59 | out, err := Run(path, "git", args...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | lines := strings.Split(out, "\n") 65 | commits := parseGitCommits(lines, time.Local) 66 | 67 | // Filter to max entries, if specified 68 | if g.filter.Max > 0 { 69 | commits = FilterMax(commits, g.filter.Max) 70 | } 71 | 72 | return commits, nil 73 | } 74 | 75 | // parseGitCommits parses output from `git log` command. 76 | func parseGitCommits(lines []string, location *time.Location) []Commit { 77 | var commits []Commit 78 | for _, str := range lines { 79 | fields := strings.SplitN(str, "|", 4) 80 | if len(fields) != 4 { 81 | fmt.Fprintln(os.Stderr, "[ERROR] Wrong commit info, skipping:", len(fields), str) 82 | continue 83 | } 84 | timestamp, err := time.ParseInLocation(RFC1123ZGit, fields[1], location) 85 | if err != nil { 86 | fmt.Fprintln(os.Stderr, "[ERROR] Cannot parse timestamp:", err) 87 | continue 88 | } 89 | commit := Commit{ 90 | Hash: fields[0], 91 | Date: timestamp, 92 | Subject: fields[3], 93 | Author: fields[2], 94 | } 95 | commits = append(commits, commit) 96 | } 97 | 98 | return commits 99 | } 100 | 101 | // SwitchTo switches to the given commit by hash. Implements VCS interface. 102 | func (g *Git) SwitchTo(hash string) error { 103 | path := g.Workspace().Path() 104 | _, err := Run(path, "git", "checkout", hash) 105 | return err 106 | } 107 | 108 | // Workspace returns assosiated Workspace. Implements VCS interface. 109 | func (g *Git) Workspace() *Workspace { 110 | return g.workspace 111 | } 112 | 113 | // Name returns vcs common name. Implements VCS interface. 114 | func (*Git) Name() string { 115 | return "git" 116 | } 117 | 118 | var ( 119 | ignoredGitArgs = []string{ 120 | "--date-order", "--author-date-order", "--topo-order", "--reverse", 121 | "--relative-date", "--date", "--parents", "--children", "--left-right", 122 | "--graph", "--show-linear-break", "--pretty", 123 | } 124 | ) 125 | 126 | // cleanGitArgs cleans user defined custom git arguments. 127 | // it basically removes arguments, that may affect formatting 128 | // output (we use specific format for parsing results) 129 | func cleanGitArgs(args ...string) []string { 130 | var ret []string 131 | for _, arg := range args { 132 | trimmed := strings.TrimSpace(arg) 133 | if trimmed == "" { 134 | continue 135 | } 136 | 137 | var ignore bool 138 | for _, ignored := range ignoredGitArgs { 139 | if strings.HasPrefix(trimmed, ignored) { 140 | ignore = true 141 | } 142 | } 143 | if !ignore { 144 | ret = append(ret, trimmed) 145 | } 146 | } 147 | return ret 148 | } 149 | 150 | // RFC1123ZGit is a git variation of RFC1123 time layout (--date=rfc) 151 | const RFC1123ZGit = "Mon, 2 Jan 2006 15:04:05 -0700" 152 | -------------------------------------------------------------------------------- /vcs_git_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestGitCleanOptions(t *testing.T) { 10 | Convey("CleanGitArgs should clean git args correctly", t, func() { 11 | Convey("clean up --pretty args", func() { 12 | args := []string{"--pretty=oneline"} 13 | cleaned := cleanGitArgs(args...) 14 | So(len(cleaned), ShouldEqual, 0) 15 | }) 16 | Convey("clean up --date args", func() { 17 | args := []string{"--date=iso"} 18 | cleaned := cleanGitArgs(args...) 19 | So(len(cleaned), ShouldEqual, 0) 20 | }) 21 | Convey("do not clean up harmless flags", func() { 22 | args := []string{"--date=iso", "--author=Ivan"} 23 | cleaned := cleanGitArgs(args...) 24 | So(len(cleaned), ShouldEqual, 1) 25 | So(cleaned[0], ShouldEqual, "--author=Ivan") 26 | }) 27 | }) 28 | } 29 | 30 | func TestGitParseCommits(t *testing.T) { 31 | Convey("Parsing commits should work correctly", t, func() { 32 | Convey("Normal case", func() { 33 | lines := []string{ 34 | `378739736dd1baa076675c02fe45822bf8936a14|Sun, 18 Oct 2015 22:49:00 +0000|Ivan Daniluk |Return again`, 35 | `4c6378260658f763d67dea3a931a25daf2a82d51|Sun, 18 Oct 2015 22:48:14 +0000|Ivan Daniluk |Returned panic`, 36 | `69814659db92a97e077196207815ca12cba7e0dd|Sun, 18 Oct 2015 22:47:54 +0000|Ivan Daniluk |Added no build`, 37 | `24ebd4283248f85746c69d1d97c7dddedd18d863|Sun, 18 Oct 2015 22:30:15 +0000|Ivan Daniluk |Remove panic`, 38 | `aec0795b436742b5669fdebd28df702704b8afb2|Sun, 18 Oct 2015 22:29:59 +0000|Ivan Daniluk |Add panic`, 39 | } 40 | commits := parseGitCommits(lines, time.UTC) 41 | So(len(commits), ShouldEqual, len(lines)) 42 | So(commits[0], ShouldResemble, Commit{ 43 | Hash: "378739736dd1baa076675c02fe45822bf8936a14", 44 | Author: "Ivan Daniluk ", 45 | Subject: "Return again", 46 | Date: time.Date(2015, time.October, 18, 22, 49, 00, 00, time.UTC), 47 | }) 48 | So(commits[4], ShouldResemble, Commit{ 49 | Hash: "aec0795b436742b5669fdebd28df702704b8afb2", 50 | Author: "Ivan Daniluk ", 51 | Subject: "Add panic", 52 | Date: time.Date(2015, time.October, 18, 22, 29, 59, 00, time.UTC), 53 | }) 54 | }) 55 | Convey("Day < 10 case", func() { 56 | lines := []string{ 57 | `378739736dd1baa076675c02fe45822bf8936a14|Sun, 8 Oct 2015 22:49:00 +0000|Ivan Daniluk |Return again`, 58 | } 59 | commits := parseGitCommits(lines, time.UTC) 60 | So(len(commits), ShouldEqual, len(lines)) 61 | So(commits[0], ShouldResemble, Commit{ 62 | Hash: "378739736dd1baa076675c02fe45822bf8936a14", 63 | Author: "Ivan Daniluk ", 64 | Subject: "Return again", 65 | Date: time.Date(2015, time.October, 8, 22, 49, 00, 00, time.UTC), 66 | }) 67 | }) 68 | Convey("Separators in subject case", func() { 69 | lines := []string{ 70 | `378739736dd1baa076675c02fe45822bf8936a14|Sun, 8 Oct 2015 22:49:00 +0000|Ivan Daniluk |Return with character'|' and again |[]|`, 71 | } 72 | commits := parseGitCommits(lines, time.UTC) 73 | So(len(commits), ShouldEqual, len(lines)) 74 | So(commits[0], ShouldResemble, Commit{ 75 | Hash: "378739736dd1baa076675c02fe45822bf8936a14", 76 | Author: "Ivan Daniluk ", 77 | Subject: "Return with character'|' and again |[]|", 78 | Date: time.Date(2015, time.October, 8, 22, 49, 00, 00, time.UTC), 79 | }) 80 | }) 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /vcs_hg.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Hg implements VCS for Mercurial version control. 12 | type Hg struct { 13 | workspace *Workspace 14 | 15 | filter FilterOptions 16 | } 17 | 18 | // NewHgVCS returns new Mercurial vcs, and checks it it's valid hg workspace. 19 | func NewHgVCS(path string, filter FilterOptions) (*Hg, error) { 20 | // check if this path is under hg control 21 | _, err := Run(path, "hg", "verify") 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | // find top-level (root) directory of workspace 27 | out, err := Run(path, "hg", "root") 28 | if err != nil { 29 | return nil, errors.New("cannot determine root folder") 30 | } 31 | root := strings.TrimSpace(out) 32 | prefix := findPrefix(path, root) 33 | 34 | workspace := NewWorkspace(root, prefix) 35 | vcs := &Hg{ 36 | workspace: workspace, 37 | filter: filter, 38 | } 39 | return vcs, nil 40 | } 41 | 42 | // Commits returns all commits for the current branch. Implements VCS interface. 43 | func (g *Hg) Commits() ([]Commit, error) { 44 | path := g.Workspace().Path() 45 | 46 | // Prepare args, and add user defined args to `hg log` command 47 | args := []string{"log", `--template={node}%{date|rfc822date}%{author}%{desc}\n`} 48 | if len(g.filter.Args) > 0 { 49 | // Append custom arguments, excluding formatting-related ones 50 | cleanedArgs := cleanHgArgs(g.filter.Args...) 51 | args = append(args, cleanedArgs...) 52 | } 53 | 54 | if g.filter.LastN > 0 { 55 | lastNArg := fmt.Sprintf("-l %d", g.filter.LastN) 56 | args = append(args, lastNArg) 57 | } 58 | 59 | out, err := Run(path, "hg", args...) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | lines := strings.Split(out, "\n") 65 | 66 | commits := parseHgCommits(lines, time.Local) 67 | 68 | // Filter to max entries, if specified 69 | if g.filter.Max > 0 { 70 | commits = FilterMax(commits, g.filter.Max) 71 | } 72 | 73 | return commits, nil 74 | } 75 | 76 | // parseHgCommits parses output from `hg log` command. 77 | func parseHgCommits(lines []string, location *time.Location) []Commit { 78 | var commits []Commit 79 | for _, str := range lines { 80 | fields := strings.SplitN(str, "%", 4) 81 | if len(fields) != 4 { 82 | fmt.Fprintln(os.Stderr, "[ERROR] Wrong commit info, skipping:", len(fields), str) 83 | continue 84 | } 85 | timestamp, err := time.ParseInLocation(time.RFC1123Z, fields[1], location) 86 | if err != nil { 87 | fmt.Fprintln(os.Stderr, "[ERROR] Cannot parse timestamp:", err) 88 | continue 89 | } 90 | commit := Commit{ 91 | Hash: fields[0], 92 | Date: timestamp, 93 | Subject: fields[3], 94 | Author: fields[2], 95 | } 96 | commits = append(commits, commit) 97 | } 98 | 99 | return commits 100 | } 101 | 102 | // SwitchTo switches to the given commit by hash. Implements VCS interface. 103 | func (g *Hg) SwitchTo(hash string) error { 104 | path := g.Workspace().Path() 105 | _, err := Run(path, "hg", "update", hash) 106 | return err 107 | } 108 | 109 | // Workspace returns assosiated Workspace. Implements VCS interface. 110 | func (g *Hg) Workspace() *Workspace { 111 | return g.workspace 112 | } 113 | 114 | // Name returns vcs common name. Implements VCS interface. 115 | func (*Hg) Name() string { 116 | return "hg" 117 | } 118 | 119 | var ( 120 | // TODO: find a person who use mercurial a lot and can 121 | // help to add more ignored args (ones that may affect 122 | // predetermined output) 123 | ignoredHgArgs = []string{ 124 | "--template", 125 | } 126 | ) 127 | 128 | // cleanHgArgs cleans user defined custom hg arguments. 129 | // it basically removes arguments, that may affect formatting 130 | // output (we use specific format for parsing results) 131 | func cleanHgArgs(args ...string) []string { 132 | var ret []string 133 | for _, arg := range args { 134 | trimmed := strings.TrimSpace(arg) 135 | if trimmed == "" { 136 | continue 137 | } 138 | 139 | var ignore bool 140 | for _, ignored := range ignoredHgArgs { 141 | if strings.HasPrefix(trimmed, ignored) { 142 | ignore = true 143 | } 144 | } 145 | if !ignore { 146 | ret = append(ret, trimmed) 147 | } 148 | } 149 | return ret 150 | } 151 | -------------------------------------------------------------------------------- /vcs_hg_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestHgParseCommits(t *testing.T) { 10 | Convey("Parsing commits should work correctly", t, func() { 11 | Convey("Normal case", func() { 12 | lines := []string{ 13 | `05a2ab6a17d3ef8ea71cddb8e4c56ce1cfcd53aa%Mon, 19 Oct 2015 02:33:30 +0000%Ivan Daniluk %new test`, 14 | `9ef3f9bab2683ff1998ec129e4b3b3a26b95ca9b%Mon, 19 Oct 2015 02:13:25 +0000%Ivan Daniluk %add bench`, 15 | `1cda6ab6b89db97aeeb27f96c61abff211956471%Thu, 14 May 2015 14:19:58 +0000%Ivan Daniluk %Updated README again`, 16 | `557ab4832839da1195a5e82c168bd5899570f37f%Thu, 14 May 2015 14:13:41 +0000%Ivan Daniluk %README.md edited online with Bitbucket`, 17 | `590df12383190d7984942de8f5503076da66d3b2%Thu, 14 May 2015 16:20:28 +0000%Ivan Daniluk %Added fmt.`, 18 | `47f015f5e63bd9018b7d581b98594f8126ca5609%Thu, 14 May 2015 16:12:55 +0000%Ivan Daniluk %Initial commit`, 19 | } 20 | commits := parseHgCommits(lines, time.UTC) 21 | So(len(commits), ShouldEqual, len(lines)) 22 | So(commits[0], ShouldResemble, Commit{ 23 | Hash: "05a2ab6a17d3ef8ea71cddb8e4c56ce1cfcd53aa", 24 | Author: "Ivan Daniluk ", 25 | Subject: "new test", 26 | Date: time.Date(2015, time.October, 19, 02, 33, 30, 00, time.UTC), 27 | }) 28 | So(commits[len(commits)-1], ShouldResemble, Commit{ 29 | Hash: "47f015f5e63bd9018b7d581b98594f8126ca5609", 30 | Author: "Ivan Daniluk ", 31 | Subject: "Initial commit", 32 | Date: time.Date(2015, time.May, 14, 16, 12, 55, 00, time.UTC), 33 | }) 34 | }) 35 | Convey("Separators in subject case", func() { 36 | lines := []string{ 37 | `378739736dd1baa076675c02fe45822bf8936a14|Sun, 8 Oct 2015 22:49:00 +0000|Ivan Daniluk |Return with character'|' and again |[]|`, 38 | } 39 | commits := parseGitCommits(lines, time.UTC) 40 | So(len(commits), ShouldEqual, len(lines)) 41 | So(commits[0], ShouldResemble, Commit{ 42 | Hash: "378739736dd1baa076675c02fe45822bf8936a14", 43 | Author: "Ivan Daniluk ", 44 | Subject: "Return with character'|' and again |[]|", 45 | Date: time.Date(2015, time.October, 8, 22, 49, 00, 00, time.UTC), 46 | }) 47 | }) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /web.go: -------------------------------------------------------------------------------- 1 | //go:generate go-bindata-assetfs assets/... 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "fmt" 8 | "github.com/elazarl/go-bindata-assetfs" 9 | "html/template" 10 | "net/http" 11 | "os" 12 | "os/exec" 13 | "reflect" 14 | "runtime" 15 | 16 | "golang.org/x/net/websocket" 17 | ) 18 | 19 | // indexTmpl is a html template for index page. 20 | var indexTmpl *template.Template 21 | 22 | func init() { 23 | indexTmpl = prepareTemplate() 24 | } 25 | 26 | // StartServer starts http-server and servers frontend code 27 | // for benchmark results display. 28 | func StartServer(bind string, ch chan interface{}, info *Info) error { 29 | // Handle static files 30 | var fs http.FileSystem 31 | if DevMode() { 32 | fs = http.Dir("assets") 33 | } else { 34 | fs = &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "assets"} 35 | } 36 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(fs))) 37 | 38 | // Index page handler 39 | http.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 40 | handler(w, r, info) 41 | })) 42 | 43 | // handle pool of websocket channels 44 | pool := make(WSPool) 45 | go func() { 46 | for { 47 | val := <-ch 48 | for _, conn := range pool { 49 | conn.ch <- val 50 | } 51 | } 52 | }() 53 | 54 | // Websocket handler 55 | http.Handle("/ws", websocket.Handler(func(ws *websocket.Conn) { 56 | wshandler(ws, &pool) 57 | })) 58 | 59 | go StartBrowser("http://localhost" + bind) 60 | 61 | return http.ListenAndServe(bind, nil) 62 | } 63 | 64 | // handler handles index page. 65 | func handler(w http.ResponseWriter, r *http.Request, info *Info) { 66 | err := indexTmpl.Execute(w, info) 67 | if err != nil { 68 | w.WriteHeader(http.StatusInternalServerError) 69 | fmt.Println("[ERROR] failed to render template:", err) 70 | return 71 | } 72 | } 73 | 74 | // StartBrowser tries to open the URL in a browser 75 | // and reports whether it succeeds. 76 | // 77 | // Orig. code: golang.org/x/tools/cmd/cover/html.go 78 | func StartBrowser(url string) bool { 79 | // try to start the browser 80 | var args []string 81 | switch runtime.GOOS { 82 | case "darwin": 83 | args = []string{"open"} 84 | case "windows": 85 | args = []string{"cmd", "/c", "start"} 86 | default: 87 | args = []string{"xdg-open"} 88 | } 89 | cmd := exec.Command(args[0], append(args[1:], url)...) 90 | fmt.Println("If browser window didn't appear, please go to this url:", url) 91 | return cmd.Start() == nil 92 | } 93 | 94 | // funcs for index template 95 | var funcs = template.FuncMap{ 96 | "last": func(a interface{}) interface{} { 97 | v := reflect.ValueOf(a) 98 | switch v.Kind() { 99 | case reflect.Slice, reflect.Array: 100 | return v.Index(v.Len() - 1).Interface() 101 | default: 102 | return nil 103 | } 104 | }, 105 | "json_stripped": func(a interface{}) template.JS { 106 | data, err := json.Marshal(a) 107 | if err != nil { 108 | fmt.Printf("[ERROR] failed to encode series: %v\n", err) 109 | return "" 110 | } 111 | data = bytes.TrimPrefix(data, []byte("{")) 112 | data = bytes.TrimSuffix(data, []byte("}")) 113 | 114 | js := template.JS(string(data)) 115 | return js 116 | }, 117 | } 118 | 119 | // DevMode returns true if app is running in development mode. 120 | func DevMode() bool { 121 | devMode := os.Getenv("GOBENCHUI_DEV") 122 | return devMode != "" 123 | } 124 | 125 | // prepareTemplate prepares and parses template. 126 | func prepareTemplate() *template.Template { 127 | t := template.New("index.html").Funcs(funcs) 128 | 129 | // read from local filesystem for development 130 | if DevMode() { 131 | return template.Must(t.ParseFiles("assets/index.html")) 132 | } 133 | 134 | data, err := Asset("assets/index.html") 135 | if err != nil { 136 | panic(err) 137 | } 138 | return template.Must(t.Parse(string(data))) 139 | } 140 | -------------------------------------------------------------------------------- /websocket.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "golang.org/x/net/websocket" 9 | ) 10 | 11 | // WSData used for WebSocket commincation with frontend. 12 | type WSData struct { 13 | Type string `json:"type"` // "status" or "result" 14 | Result BenchmarkSet `json:"result,omitempty"` 15 | 16 | Status Status `json:"status,omitempty"` 17 | Progress float64 `json:"progress,omitempty"` 18 | 19 | Commit Commit `json:"commit,omitempty"` 20 | StartTime time.Time `json:"start_time,omitempty"` 21 | 22 | Error error `json:"error,omitempty"` 23 | } 24 | 25 | // wshandler is a handler for websocket connection. 26 | func wshandler(ws *websocket.Conn, pool *WSPool) { 27 | conn := pool.Register(ws) 28 | defer func() { 29 | fmt.Println("[DEBUG] Closing connection") 30 | pool.Deregister(conn) 31 | }() 32 | 33 | for { 34 | val, ok := <-conn.ch 35 | if !ok { 36 | return 37 | } 38 | 39 | var data WSData 40 | 41 | switch val.(type) { 42 | case BenchmarkRun: 43 | status := val.(BenchmarkRun) 44 | data = WSData{ 45 | Type: "status", 46 | Status: InProgress, 47 | Commit: status.Commit, 48 | Error: status.Error, 49 | } 50 | case BenchmarkStatus: 51 | status := val.(BenchmarkStatus) 52 | data = WSData{ 53 | Type: "status", 54 | Status: status.Status, 55 | Progress: status.Progress, 56 | } 57 | case BenchmarkSet: 58 | data = WSData{ 59 | Type: "result", 60 | Result: val.(BenchmarkSet), 61 | Status: InProgress, 62 | } 63 | } 64 | 65 | if err := sendJSON(ws, data); err != nil { 66 | return 67 | } 68 | } 69 | } 70 | 71 | // sendJSON is a wrapper for sending JSON encoded data to websocket 72 | func sendJSON(ws *websocket.Conn, data interface{}) error { 73 | body, err := json.MarshalIndent(data, " ", " ") 74 | if err != nil { 75 | fmt.Println("[ERROR] JSON encoding failed", err) 76 | return err 77 | } 78 | 79 | _, err = ws.Write(body) 80 | if err != nil { 81 | // skip silently, as it's normal when client disconnects 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // WSConn represents single websocket connection. 89 | type WSConn struct { 90 | id int64 91 | ws *websocket.Conn 92 | ch chan interface{} 93 | } 94 | 95 | // WSPool holds registered websocket connections. 96 | type WSPool map[int64]*WSConn 97 | 98 | // Register registers new websocket connection and creates new channel for it. 99 | func (pool WSPool) Register(ws *websocket.Conn) *WSConn { 100 | ch := make(chan interface{}) 101 | id := time.Now().UnixNano() 102 | wsConn := &WSConn{ 103 | id: id, 104 | ws: ws, 105 | ch: ch, 106 | } 107 | 108 | pool[id] = wsConn 109 | 110 | return wsConn 111 | } 112 | 113 | // Deregister removes connection from pool. 114 | func (pool WSPool) Deregister(conn *WSConn) { 115 | for id, c := range pool { 116 | if id == conn.id { 117 | c.ws.Close() 118 | close(c.ch) 119 | delete(pool, id) 120 | return 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /workspace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | // Workspace represents local top-level VCS workspace. 13 | // 14 | // Package may be in subfolder (like github.com/etcd/coreos/store), 15 | // but Workspace represents the whole directory (github.com/etcd/coreos). 16 | // 17 | // Following this example, root is '~/github.com/etcd/coreos', 18 | // prefix is 'store'. 19 | // 20 | // gopath is introduced to handle GO15VENDOREXPERIMENT gopath issue, 21 | // if root is '/tmp/tempXXXX/src/pkg/github.com/etcd/coreos', 22 | // then gopath is '/tmp/tempXXXX'. Optional. 23 | type Workspace struct { 24 | root string 25 | prefix string 26 | 27 | gopath string 28 | } 29 | 30 | // NewWorkspace creates new Workspace. 31 | func NewWorkspace(root, prefix string) *Workspace { 32 | return &Workspace{ 33 | root: root, 34 | prefix: prefix, 35 | } 36 | } 37 | 38 | // Path returns full workspace path to package (w/ prefix). 39 | func (w *Workspace) Path() string { 40 | return filepath.Join(w.root, w.prefix) 41 | } 42 | 43 | // Root returns root directory for workspace (w/o prefix). 44 | func (w *Workspace) Root() string { 45 | return w.root 46 | } 47 | 48 | // Gopath returns root gopath directory for workspace (w/o prefix). 49 | func (w *Workspace) Gopath() string { 50 | return w.gopath 51 | } 52 | 53 | // SetRoot sets new root path for workspace. 54 | func (w *Workspace) SetRoot(gopath, root string) { 55 | w.gopath = gopath 56 | w.root = root 57 | } 58 | 59 | // Clone copies whole workspace to temporary directory. 60 | func (w *Workspace) Clone() error { 61 | tmp, err := ioutil.TempDir("", ProgramName) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | fmt.Println("[DEBUG] Cloning git workspace to", tmp) 67 | // place sources under src/pkg to make it look like 68 | // proper GOPATH (needed for GO15VENDOREXPERIMENT support) 69 | targetDir := filepath.Join(tmp, "src", "pkg") 70 | err = os.MkdirAll(targetDir, os.ModePerm) 71 | if err != nil { 72 | os.RemoveAll(tmp) 73 | return err 74 | } 75 | err = copyAll(targetDir+"/", w.Root()) 76 | if err != nil { 77 | os.RemoveAll(tmp) 78 | return err 79 | } 80 | w.SetRoot(tmp, targetDir) 81 | 82 | return nil 83 | } 84 | 85 | // copyFile copies the file with path src to dst. The new file must not exist. 86 | // It is created with the same permissions as src. 87 | func copyFile(dst, src string) error { 88 | rf, err := os.Open(src) 89 | if err != nil { 90 | return err 91 | } 92 | defer rf.Close() 93 | rstat, err := rf.Stat() 94 | if err != nil { 95 | return err 96 | } 97 | if rstat.IsDir() { 98 | return fmt.Errorf("dir argument to CopyFile (%s, %s)", dst, src) 99 | } 100 | 101 | wf, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, rstat.Mode()) 102 | if err != nil { 103 | return err 104 | } 105 | if _, err := io.Copy(wf, rf); err != nil { 106 | wf.Close() 107 | return err 108 | } 109 | return wf.Close() 110 | } 111 | 112 | // copyAll copies the file or (recursively) the directory at src to dst. 113 | // Permissions are preserved. dst must already exist. 114 | func copyAll(dst, src string) error { 115 | return filepath.Walk(src, makeWalkFn(dst, src)) 116 | } 117 | 118 | func makeWalkFn(dst, src string) filepath.WalkFunc { 119 | return func(path string, info os.FileInfo, err error) error { 120 | if err != nil { 121 | return err 122 | } 123 | dir := strings.TrimPrefix(path, src) 124 | if dir == "/" || dir == "" { 125 | return nil 126 | } 127 | dstPath := filepath.Join(dst, dir) 128 | if info.IsDir() { 129 | return os.Mkdir(dstPath, info.Mode()) 130 | } 131 | if info.Mode()&os.ModeSymlink == os.ModeSymlink { 132 | if newPath, err := os.Readlink(path); err != nil { 133 | return err 134 | } else { 135 | if newPath[0] != '/' { 136 | // Relative symlink 137 | path = filepath.Join(filepath.Dir(path), newPath) 138 | } else { 139 | path = newPath 140 | } 141 | 142 | if info, err = os.Lstat(path); err != nil { 143 | return err 144 | } 145 | 146 | if info.IsDir() { 147 | if err = os.Mkdir(dstPath, info.Mode()); err != nil { 148 | return err 149 | } 150 | // Following the dir symlink 151 | return filepath.Walk(path, makeWalkFn(dstPath, path)) 152 | } 153 | } 154 | } 155 | return copyFile(dstPath, path) 156 | } 157 | } 158 | --------------------------------------------------------------------------------