├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── files-to-c-arrays.py ├── generate-project-files ├── generate-project-files.bat ├── html ├── 2048.png ├── compatibility.js ├── draw-pattern.js ├── gradient.png ├── hardware-latency-test-bookmarklet.js ├── hardware-latency-test.html ├── index.html ├── keep-server-alive.js ├── latency-benchmark.css ├── latency-benchmark.html ├── latency-benchmark.js └── worker.js ├── latency-benchmark.gyp ├── linux-build └── src ├── clioptions.c ├── clioptions.h ├── latency-benchmark.c ├── latency-benchmark.h ├── mac ├── main.m └── screenscraper.m ├── oculus.cpp ├── oculus.h ├── screenscraper.h ├── server.c ├── win ├── getopt.c ├── main.cpp ├── screenscraper.cpp └── stdafx.h └── x11 ├── main.c └── screenscraper.c /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third_party/mongoose"] 2 | path = third_party/mongoose 3 | url = https://github.com/valenok/mongoose.git 4 | [submodule "third_party/gyp"] 5 | path = third_party/gyp 6 | url = https://chromium.googlesource.com/external/gyp.git 7 | [submodule "third_party/LibOVR"] 8 | path = third_party/LibOVR 9 | url = git@github.com:jdarpinian/LibOVR.git 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](http://google.github.io/latency-benchmark/screenshot.png "Web Latency Benchmark") 2 | 3 | ## About the benchmark 4 | 5 | The Web Latency Benchmark is a new kind of benchmark that tests your browser's responsiveness by directly measuring *latency* and *jank*. Visit the homepage at http://google.github.io/latency-benchmark for examples of the kinds of latency and jank that are measured. 6 | 7 | * Download for Windows: [latency-benchmark.exe](http://google.github.io/latency-benchmark/latency-benchmark.exe) 8 | * Download for Mac: [latency-benchmark-mac.zip](http://google.github.io/latency-benchmark/latency-benchmark-mac.zip) 9 | * Download for Linux: [latency-benchmark-linux.zip](http://google.github.io/latency-benchmark/latency-benchmark-linux.zip) 10 | 11 | ## New: Oculus Latency Tester support 12 | 13 | The [Oculus Latency Tester](https://www.oculus.com/blog/latency-tester-pre-orders-now-open/) is a hardware device with a light sensor that can measure end-to-end latency from USB input to pixels changing on the screen. This kind of hardware-based measurement accounts for all possible sources of latency. It's the most complete and accurate measurement possible, and it's now supported by the Web Latency Benchmark. Just plug it in and you'll see a special test page. 14 | 15 | ## New: Automated testing 16 | 17 | Thanks to jmaher, the benchmark now accepts command-line arguments that enable fully automated benchmark runs, with results reported in JSON format to a server of your choosing. 18 | 19 | ## How it works 20 | 21 | The Web Latency Benchmark works by programmatically sending input events to a browser window, and using screenshot APIs to detect when the browser has finished drawing its response. 22 | 23 | There are two main components: the latency-benchmark server (written in C/C++) and the HTML/JavaScript benchmark page. The HTML page draws a special pattern of pixels to the screen using WebGL or Canvas 2D, then makes an XMLHTTPRequest to the server to start the latency measurement. The server locates the browser window by searching a screenshot for the special pattern. Once the browser window is located, the server starts sending input events. Each time the HTML page receives an input event it encodes that information into pixels in the on-screen pattern, drawn using the canvas element. Meanwhile the server is taking screenshots every few milliseconds. By decoding the pixel pattern the server can determine to within a few milliseconds how long it takes the browser to respond to each input event. 24 | 25 | The native reference test is special because it requires extra support from the server. Using the native APIs of each platform, the server creates a special benchmark window that draws the same pattern as the test webpage, and responds to keyboard input in the same way. To ensure fairness when compared with the browser, this window is opened in a separate process and uses OpenGL to draw the pattern on the screen. The benchmark window opens as a popup window, only 1 pixel tall and without a border or title bar, so it's almost unnoticeable. 26 | 27 | ## License and distribution 28 | 29 | The Web Latency Benchmark is licensed under the Apache License version 2.0. This is an open source project; it is not an official Google product. 30 | 31 | ## Build prerequisites 32 | 33 | Python 2.x is required on all platforms for GYP, which generates the build files. 34 | 35 | * Windows: GYP is currently configured to generate project files for Visual Studio 2012 (Express works). 2010 might work too if you edit generate-project-files.bat to change the version. The Windows 8 SDK is required due to the use of DXGI 1.2. It can be installed on Windows 7 and Windows Vista. 36 | * Mac: XCode 4 is required. 37 | * Linux: Clang is required. The benchmark does not compile with GCC. Other build dependencies are development headers for OpenGL, X11, and udev (for the Oculus SDK). The corresponding Debian/Ubuntu packages are libgl1-mesa-dev, xorg-dev, and libudev-dev. 38 | 39 | ## Build steps 40 | 41 | First, you need to `git submodule init && git submodule update` to fetch the submodules in third_party. Then, you need to run `generate-project-files`, which will run GYP and generate platform-specific project files in build/. 42 | 43 | * Windows: Open `build/latency-benchmark.sln`. 44 | * Mac: Open `build/latency-benchmark.xcodeproj`. For debugging you will need to edit the default scheme to change the working directory of the `latency-test` executable to `$(PROJECT_DIR)` so it can find the HTML files. You will also want to [configure the debugger to ignore SIGPIPE](http://stackoverflow.com/questions/10431579/permanently-configuring-lldb-in-xcode-4-3-2-not-to-stop-on-signals). 45 | * Linux: Run the script `linux-build` to compile with Clang. The binary will be built at `build/out/Debug/latency-benchmark`. Run it in the top-level directory so it can find the HTML files. You can build the release version by defining the environment variable `BUILDTYPE=Release`. 46 | 47 | You shouldn't make any changes to the XCode or Visual Studio project files directly. Instead, you should edit `latency-benchmark.gyp` to reflect the changes you want, and re-run the `generate-project-files` script to update the project files with the changes. This ensures that the project files stay in sync across platforms. 48 | 49 | ## TODO 50 | 51 | * Bookmarklet or browser extension for injecting a hardware latency test into any web page. 52 | * Support for more operating systems: 53 | * Android 54 | * iOS 55 | * Chrome OS 56 | * Support jank measurement with the Oculus Latency Tester, in addition to latency measurement. 57 | * Fix non-Firefox browsers in automated testing mode on Windows (mousewheel scroll events not sent properly). 58 | * Fix IE11 in high DPI mode (test pattern scrolls off screen). 59 | * Disable mouse and keyboard input during the test to avoid interference. 60 | * Hide the mouse cursor during the test. 61 | * Find a way to share constants like the test timeout between JS and C. 62 | * Test more possible causes of jank: 63 | * audio/video loading 64 | * plugins 65 | * JavaScript parsing 66 | * GC 67 | * WebGL shader compilation 68 | * Web Worker JavaScript parsing/execution 69 | * GC in a worker 70 | * HTML parsing 71 | * CSS parsing 72 | * layout 73 | * DNS resolution 74 | * window resizing 75 | * image resizing 76 | * XHR starting/ending 77 | * All of the above in an iframe or popup. 78 | -------------------------------------------------------------------------------- /files-to-c-arrays.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright 2013 Google Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import sys 18 | 19 | if len(sys.argv) < 3: 20 | print 'Usage: ' + sys.argv[0] + 'output_file input_file1 input_file2 ... input_fileN' 21 | print 22 | print 'Generates a .c file containing all of the input files as static' 23 | print 'character arrays, along with a function to retrieve them.' 24 | print 25 | print 'const char *get_file(const char *path, size_t *out_size)' 26 | exit(1) 27 | 28 | 29 | def chunk(list, n): 30 | """Split a list into size n chunks (the last chunk may be shorter).""" 31 | return (list[i : i + n] for i in range(0, len(list), n)) 32 | 33 | filesizes = [] 34 | filepaths = [] 35 | filearrays = [] 36 | for filepath in sys.argv[2:]: 37 | filepaths.append(filepath.replace('\\', '/').lstrip('./')) 38 | file = open(filepath, 'rb').read() 39 | filesizes.append(len(file)) 40 | escapedfile = '\\x' + '\\x'.join(chunk(file.encode('hex'), 2)) 41 | filearrays.append('"\n "'.join(chunk(escapedfile, 76))) 42 | 43 | template = """#include 44 | #include 45 | 46 | static const char *file_paths[] = {"%s"}; 47 | static const size_t file_sizes[] = {%s}; 48 | static const int num_files = %d; 49 | static const char *files[] = { 50 | "%s" 51 | }; 52 | 53 | const char *get_file(const char *path, size_t *out_size) { 54 | for (int i = 0; i < num_files; i++) { 55 | if (strcmp(file_paths[i], path) == 0) { 56 | *out_size = file_sizes[i]; 57 | return files[i]; 58 | } 59 | } 60 | return NULL; 61 | } 62 | """ 63 | 64 | output = open(sys.argv[1], 'w') 65 | output.write(template % ('", "'.join(filepaths), 66 | ', '.join(str(x) for x in filesizes), 67 | len(filepaths), 68 | '",\n "'.join(filearrays))) 69 | -------------------------------------------------------------------------------- /generate-project-files: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$( dirname "${BASH_SOURCE[0]}" )" 3 | # Auto regeneration is disabled because it is broken in GYP when using 4 | # --generator-output. 5 | ./third_party/gyp/gyp \ 6 | --depth=. \ 7 | --generator-output=build \ 8 | -Gauto_regeneration=0 \ 9 | -f make \ 10 | -f xcode 11 | -------------------------------------------------------------------------------- /generate-project-files.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | pushd "%~dp0" 3 | python third_party/gyp/gyp ^ 4 | --depth=. ^ 5 | --generator-output=build ^ 6 | -G msvs_version=2012e ^ 7 | -f msvs 8 | popd 9 | -------------------------------------------------------------------------------- /html/2048.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/latency-benchmark/ff3118720b0d1901abc6148627394bdbedaa8731/html/2048.png -------------------------------------------------------------------------------- /html/compatibility.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var capitalize = function(s) { 18 | var firstLetter = s.slice(0, 1); 19 | var rest = s.slice(1); 20 | return firstLetter.toUpperCase() + rest; 21 | }; 22 | 23 | var uppercasePrefixes = ['', 'Moz', 'ms', 'o', 'WebKit']; 24 | var lowercasePrefixes = uppercasePrefixes.slice(0); 25 | for (var i = 0; i < lowercasePrefixes.length; i++) { 26 | lowercasePrefixes[i] = lowercasePrefixes[i].toLowerCase(); 27 | } 28 | var cssPrefixes = ['']; 29 | for (var i = 1; i < lowercasePrefixes.length; i++) { 30 | cssPrefixes.push('-' + lowercasePrefixes[i] + '-'); 31 | } 32 | var prefixes = lowercasePrefixes.concat(uppercasePrefixes.slice(1)); 33 | 34 | var getPrefixed = function(toCheck, on) { 35 | if (!on) 36 | return undefined; 37 | for (var i = 0; i < prefixes.length; i++) { 38 | var prefix = prefixes[i]; 39 | var checked = on[prefix + toCheck]; 40 | if (checked !== undefined) return checked; 41 | checked = on[prefix + capitalize(toCheck)]; 42 | if (checked !== undefined) return checked; 43 | } 44 | return undefined; 45 | }; 46 | 47 | var hasProperty = function(toCheck, on) { 48 | return on.hasOwnProperty(toCheck) || (Object.getPrototypeOf(on) && hasProperty(toCheck, Object.getPrototypeOf(on))); 49 | }; 50 | 51 | var setPrefixed = function(toSet, value, on) { 52 | for (var i = 0; i < prefixes.length; i++) { 53 | var prefix = prefixes[i]; 54 | if (hasProperty(prefix + toSet, on)) { 55 | on[prefix + toSet] = value; 56 | return true; 57 | } 58 | if (hasProperty(prefix + capitalize(toSet), on)) { 59 | on[prefix + capitalize(toSet)] = value; 60 | return true; 61 | } 62 | } 63 | return false; 64 | }; 65 | 66 | if (!window.devicePixelRatio) { 67 | window.devicePixelRatio = 1; 68 | } 69 | 70 | var performanceNow = getPrefixed('now', window.performance); 71 | if (performanceNow) { 72 | window.performance.now = performanceNow; 73 | } 74 | 75 | var getMs = function() { 76 | if(performanceNow) 77 | return window.performance.now(); 78 | else 79 | return new Date().getTime(); 80 | }; 81 | -------------------------------------------------------------------------------- /html/draw-pattern.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // We draw a pattern on the screen, encoding information in the colors that can be read by the server in screenshots. We can encode three bytes per pixel, ignoring the alpha channel. The pattern starts with a "magic" identification number, then encodes information about the state of the page. 18 | 19 | // Make the page background a repeating gradient from rgb(0, 0, 0) to rgb(255, 255, 255). This allows the server to read the page's scroll position (mod 255). This background will be almost entirely covered by other content, leaving only one pixel visible for the server to read. 20 | document.body.style.backgroundImage = 'url("gradient.png")'; 21 | document.body.style.backgroundSize = '1px ' + 256 / window.devicePixelRatio + 'px'; 22 | document.body.style.height = '1000000px'; 23 | 24 | var testContainer = document.createElement('div'); 25 | setPrefixed('transformOrigin', 'top left', testContainer.style); 26 | setPrefixed('transform', 'scale(' + (1 / window.devicePixelRatio) + ')', testContainer.style); 27 | setPrefixed('position', 'fixed', testContainer.style); 28 | setPrefixed('top', '0px', testContainer.style); 29 | setPrefixed('left', '1px', testContainer.style); 30 | setPrefixed('width', '1000%', testContainer.style); 31 | setPrefixed('height', '10px', testContainer.style); 32 | setPrefixed('overflow', 'hidden', testContainer.style); 33 | 34 | var canvas2d = document.createElement('canvas'); 35 | var context2D = canvas2d.getContext('2d'); 36 | var canvasGL = document.createElement('canvas'); 37 | setPrefixed('position', 'absolute', canvasGL.style); 38 | setPrefixed('top', '0px', canvasGL.style); 39 | setPrefixed('left', '0px', canvasGL.style); 40 | canvasGL.width = 500; 41 | canvasGL.height = 1; 42 | var gl = null; 43 | try { 44 | gl = canvasGL.getContext('webgl') || 45 | canvasGL.getContext('experimental-webgl') || 46 | canvasGL.getContext('moz-webgl') || 47 | canvasGL.getContext('o-webgl') || 48 | canvasGL.getContext('ms-webgl') || 49 | canvasGL.getContext('webkit-3d') || 50 | canvasGL.getContext('3d'); 51 | } catch (e) {} 52 | if (!gl) { 53 | var notgl = canvasGL.getContext('2d'); 54 | } 55 | testContainer.appendChild(canvasGL); 56 | 57 | 58 | var gradientImage = document.createElement('img'); 59 | gradientImage.src = 'gradient.png'; 60 | setPrefixed('position', 'absolute', gradientImage.style); 61 | setPrefixed('top', '0', gradientImage.style); 62 | setPrefixed('left', '0', gradientImage.style); 63 | setPrefixed('animationDuration', '4.26666667s', gradientImage.style); // 1 pixel per second 64 | setPrefixed('animationName', 'gradientImage', gradientImage.style); 65 | setPrefixed('animationTimingFunction', 'linear', gradientImage.style); 66 | setPrefixed('animationIterationCount', 'infinite', gradientImage.style); 67 | setPrefixed('transformOrigin', '0px 0px 0px', gradientImage.style); 68 | 69 | var keyframesCss = '@{prefix}keyframes gradientImage {' + 70 | 'from {{prefix}transform: translate(6px, 0px); }' + 71 | 'to {{prefix}transform: translate(6px, -255px); }}'; 72 | var keyframesCssWithPrefix = ''; 73 | for (var i = 0; i < cssPrefixes.length; i++) { 74 | keyframesCssWithPrefix += keyframesCss.replace(/{prefix}/g, cssPrefixes[i]); 75 | } 76 | var newStyleSheet = document.createElement('style'); 77 | newStyleSheet.textContent = keyframesCssWithPrefix; 78 | document.head.appendChild(newStyleSheet); 79 | testContainer.appendChild(gradientImage); 80 | var rightBlocker = document.createElement('div'); 81 | var bottomBlocker = document.createElement('div'); 82 | rightBlocker.style.position = 'absolute'; 83 | rightBlocker.style.left = '7px'; 84 | rightBlocker.style.top = '0px'; 85 | rightBlocker.style.background = 'black'; 86 | rightBlocker.style.width = '100%'; 87 | rightBlocker.style.height = '100%'; 88 | bottomBlocker.style.position = 'absolute'; 89 | bottomBlocker.style.left = '0px'; 90 | bottomBlocker.style.top = '1px'; 91 | bottomBlocker.style.background = 'black'; 92 | bottomBlocker.style.width = '100%'; 93 | bottomBlocker.style.height = '100%'; 94 | testContainer.appendChild(rightBlocker); 95 | testContainer.appendChild(bottomBlocker); 96 | document.body.appendChild(testContainer); 97 | var leftBlocker = document.createElement('div'); 98 | leftBlocker.style.position = 'fixed'; 99 | leftBlocker.style.left = '0px'; 100 | leftBlocker.style.top = '0px'; 101 | leftBlocker.style.background = 'black'; 102 | leftBlocker.style.width = '1px'; 103 | leftBlocker.style.height = '10px'; 104 | document.body.appendChild(leftBlocker); 105 | 106 | var zPresses = 0; 107 | window.onkeydown = function(e) { 108 | if (e.keyCode == 90) { 109 | zPresses++; 110 | } 111 | // If Esc is pressed, abort the current test. 112 | if (e.keyCode == 27) { 113 | testMode = TEST_MODES.ABORT; 114 | } 115 | }; 116 | 117 | 118 | var frames = 0; 119 | var patternPixels = 5; 120 | var patternBytes = patternPixels * 3; 121 | var randomByte = function() { 122 | return (Math.random() * 256) | 0; 123 | } 124 | var magicPattern = [ 138, 54, 5, 45, 2, 197, randomByte(), randomByte(), randomByte(), randomByte(), randomByte(), randomByte() ]; 125 | var byteToHex = function(byte) { 126 | var hex = byte.toString(16); 127 | if(hex.length == 1) hex = '0' + hex; 128 | return hex; 129 | }; 130 | var magicPatternHex = ''; 131 | for (var i = 0; i < magicPattern.length; i++) { 132 | magicPatternHex += byteToHex(magicPattern[i]); 133 | } 134 | var raf = getPrefixed('requestAnimationFrame', window) || function(callback) { window.setTimeout(callback, 16.67); }; 135 | var patternBuffer = new ArrayBuffer(patternBytes); 136 | var patternByteArray = new Uint8Array(patternBuffer); 137 | patternByteArray.set(magicPattern); 138 | var testMode = 0; 139 | var TEST_MODES = { 140 | JAVASCRIPT_LATENCY: 1, 141 | SCROLL_LATENCY: 2, 142 | PAUSE_TIME: 3, 143 | PAUSE_TIME_TEST_FINISHED: 4, 144 | NATIVE_REFERENCE: 5, 145 | ABORT: 6, 146 | } 147 | var callback = function() { 148 | raf(callback); 149 | frames++; 150 | patternByteArray[magicPattern.length + 0] = frames; 151 | patternByteArray[magicPattern.length + 1] = zPresses; 152 | patternByteArray[magicPattern.length + 2] = testMode; 153 | if (gl) { 154 | gl.clearColor(0, 0, 0, 0); 155 | gl.disable(gl.SCISSOR_TEST); 156 | gl.clear(gl.COLOR_BUFFER_BIT); 157 | gl.enable(gl.SCISSOR_TEST); 158 | for (var i = 0; i < patternByteArray.length - 2; i += 3) { 159 | gl.scissor(i / 3, 0, 1, 100); 160 | gl.clearColor(patternByteArray[i + 2] / 255, patternByteArray[i + 1] / 255, patternByteArray[i + 0] / 255, 1); 161 | gl.clear(gl.COLOR_BUFFER_BIT); 162 | } 163 | } else { 164 | notgl.clearRect(0, 0, 10000, 10000); 165 | for (var i = 0; i < patternByteArray.length - 2; i += 3) { 166 | notgl.fillStyle = "rgb(" + patternByteArray[i + 2] + ',' + patternByteArray[i + 1] + ',' + patternByteArray[i + 0] + ')'; 167 | notgl.fillRect(i / 3, 0, 1, 1); 168 | } 169 | } 170 | }; 171 | callback(); 172 | -------------------------------------------------------------------------------- /html/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/latency-benchmark/ff3118720b0d1901abc6148627394bdbedaa8731/html/gradient.png -------------------------------------------------------------------------------- /html/hardware-latency-test-bookmarklet.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | if (window.latencyTestEmbedded) return; 3 | window.latencyTestEmbedded = true; 4 | 5 | var button = document.createElement('button'); 6 | button.style.cssText = 'position:fixed !important; z-index: 9999999 !important; color: black !important; background: white !important; border: 10px solid gray !important; padding: 10px !important; top: 100px !important; left: 100px; !important'; 7 | button.onclick = buttonPress; 8 | button.textContent = 'Click here to focus page and show test area.'; 9 | document.body.appendChild(button); 10 | 11 | function buttonPress() { 12 | document.body.removeChild(button); 13 | var testCanvas = document.createElement('canvas'); 14 | testCanvas.style.cssText = 'position:fixed !important; z-index: 9999999 !important; border: 10px solid gray !important; top: 100px !important; left: 100px; !important'; 15 | testCanvas.width = 300; 16 | testCanvas.height = 300; 17 | 18 | var gl = testCanvas.getContext('webgl') || testCanvas.getContext('experimental-webgl'); 19 | if (!gl) { 20 | alert('Failed to initialize WebGL.'); 21 | return; 22 | } 23 | document.body.appendChild(testCanvas); 24 | var color = [0, 0, 0]; 25 | window.addEventListener('keydown', function(e) { 26 | if (e.keyCode == 66) { 27 | // 'B' for black. 28 | color = [0, 0, 0]; 29 | } 30 | if (e.keyCode == 87) { 31 | // 'W' for white. 32 | color = [1, 1, 1]; 33 | } 34 | if (e.keyCode == 84) { 35 | // 'T' for test. 36 | startTest(); 37 | } 38 | e.preventDefault(); 39 | e.stopPropagation(); 40 | return false; 41 | }, true); 42 | 43 | var testRunning = false; 44 | 45 | function draw() { 46 | if (testRunning) { 47 | requestAnimationFrame(draw); 48 | } 49 | gl.clearColor(color[0], color[1], color[2], 1); 50 | gl.clear(gl.COLOR_BUFFER_BIT); 51 | } 52 | draw(); 53 | 54 | var results = []; 55 | function startTest() { 56 | if (testRunning) return; 57 | testRunning = true; 58 | requestAnimationFrame(draw); 59 | var request = new XMLHttpRequest(); 60 | request.open('GET', 'http://localhost:5578/oculusLatencyTester?defeatCache=' + Math.random(), true); 61 | request.onreadystatechange = function() { 62 | if (request.readyState == 4) { 63 | if (request.status == 200) { 64 | console.log(request.response); 65 | } 66 | testRunning = false; 67 | } 68 | }; 69 | request.send(); 70 | } 71 | } 72 | })(); 73 | -------------------------------------------------------------------------------- /html/hardware-latency-test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Latency Benchmark 5 | 6 | 9 | 10 |

Web Latency Benchmark: Hardware test

11 | 12 | Oculus Latency Tester detected! Point it at the square on the left and press the test button, or 'T' on the keyboard, to run the test. 13 |

14 | To embed a hardware latency test on any page, use this bookmarklet: Insert latency test 15 |

16 | 17 | 18 |

19 |

20 |
21 | 22 | 23 | 113 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Latency Benchmark 5 | 6 | 16 | 17 |

Web Latency Benchmark

18 | 19 |
20 |

Click here to start the test

21 | The benchmark server is running. Load the test page in any browser on the local machine to begin the test. 22 |

23 | If you have an Oculus Latency Tester, plug it in now to perform a hardware latency test. 24 |

25 | 26 |
27 | The benchmark server is no longer running. You must restart it before running the test. 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /html/keep-server-alive.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | var keepServerAliveRequestsSent = 0; 18 | var keepServerAliveRequest = null; 19 | var serverStatusAlive = document.getElementById('serverStatusAlive'); 20 | var serverStatusDead = document.getElementById('serverStatusDead'); 21 | var running = false; 22 | var pageNavigated = false; 23 | window.onbeforeunload = function() { 24 | pageNavigated = true; 25 | } 26 | function sendKeepServerAliveRequest() { 27 | keepServerAliveRequest = new XMLHttpRequest(); 28 | keepServerAliveRequest.open('GET', '/keepServerAlive?randomNumber=' + Math.random(), true); 29 | keepServerAliveRequest.onreadystatechange = function() { 30 | if (pageNavigated) return; 31 | if (!running && keepServerAliveRequest.readyState < 4 && keepServerAliveRequest.readyState > 2) { 32 | running = true; 33 | if (serverStatusAlive && serverStatusDead) { 34 | serverStatusAlive.style.display = 'block'; 35 | serverStatusDead.style.display = 'none'; 36 | } 37 | } 38 | if (keepServerAliveRequest.readyState == 4) { 39 | running = false; 40 | if (serverStatusAlive && serverStatusDead) { 41 | serverStatusAlive.style.display = 'none'; 42 | serverStatusDead.style.display = 'block'; 43 | } 44 | window.setTimeout(sendKeepServerAliveRequest, 250); 45 | } 46 | }; 47 | keepServerAliveRequest.onprogress = function() { 48 | if (keepServerAliveRequest.readyState == 3) { 49 | var response = keepServerAliveRequest.response; 50 | var last = response.slice(response.length - 1); 51 | var latencyTester = last == '1'; 52 | if (latencyTester && !/hardware-latency-test/.test(window.location.pathname)) { 53 | window.location.href = '/hardware-latency-test.html'; 54 | } 55 | if (!latencyTester && /hardware-latency-test/.test(window.location.pathname)) { 56 | window.location.href = '/'; 57 | } 58 | } 59 | } 60 | keepServerAliveRequest.send(); 61 | } 62 | sendKeepServerAliveRequest(); 63 | -------------------------------------------------------------------------------- /html/latency-benchmark.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | body { 18 | font-family: sans-serif; 19 | font-size: 14px; 20 | background-color: black; 21 | color: #CCC; 22 | } 23 | h1 { 24 | font-variant: small-caps; 25 | font-size: 30px; 26 | } 27 | a { 28 | color: #39c; 29 | } 30 | a:visited { 31 | color: #39c; 32 | } 33 | .testName { 34 | text-align: right; 35 | width: 300px; 36 | } 37 | .testResult { 38 | width: 35px; 39 | height: 40px; 40 | } 41 | .testResult div { 42 | font-size: 180%; 43 | text-align: center; 44 | } 45 | .passed { 46 | color: green; 47 | visibility: hidden; 48 | } 49 | .failed { 50 | color: red; 51 | } 52 | .testError { 53 | color: red; 54 | } 55 | .message { 56 | margin: 30px; 57 | height: 40px; 58 | font-size: 26px; 59 | text-align:center; 60 | color: white; 61 | } 62 | #score { 63 | font-size: 140%; 64 | } 65 | .testDescription { 66 | color: #777; 67 | font-size: 10px; 68 | } 69 | #onScreenCanvas { 70 | position:fixed; 71 | top: 0px; 72 | left: 1px; 73 | -webkit-user-select: none; 74 | -khtml-user-select: none; 75 | -moz-user-select: none; 76 | -ms-user-select: none; 77 | user-select: none; 78 | } 79 | #serverStatus { 80 | color: green; 81 | } 82 | #tests { 83 | font-size: 16px; 84 | table-layout: fixed; 85 | width: 100%; 86 | } 87 | #background { 88 | position:fixed; 89 | top: 2px; 90 | left: 0px; 91 | width: 90%; 92 | height: 100%; 93 | padding-left: 5%; 94 | padding-right: 10%; 95 | padding-top: 10px; 96 | background-color: black; 97 | } 98 | -------------------------------------------------------------------------------- /html/latency-benchmark.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Web Latency Benchmark 5 | 6 | 7 |
8 |

Web Latency Benchmark

9 |
Running tests... Press Esc to abort.
10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /html/latency-benchmark.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | parseQueryString = function(encodedString, useArrays) { 19 | // strip a leading '?' from the encoded string 20 | var qstr = (encodedString[0] == "?") ? encodedString.substring(1) : 21 | encodedString; 22 | var pairs = qstr.replace(/\+/g, "%20").split(/(\&\;|\&\#38\;|\&|\&)/); 23 | var o = {}; 24 | var decode; 25 | if (typeof(decodeURIComponent) != "undefined") { 26 | decode = decodeURIComponent; 27 | } else { 28 | decode = unescape; 29 | } 30 | if (useArrays) { 31 | for (var i = 0; i < pairs.length; i++) { 32 | var pair = pairs[i].split("="); 33 | if (pair.length !== 2) { 34 | continue; 35 | } 36 | var name = decode(pair[0]); 37 | var arr = o[name]; 38 | if (!(arr instanceof Array)) { 39 | arr = []; 40 | o[name] = arr; 41 | } 42 | arr.push(decode(pair[1])); 43 | } 44 | } else { 45 | for (i = 0; i < pairs.length; i++) { 46 | pair = pairs[i].split("="); 47 | if (pair.length !== 2) { 48 | continue; 49 | } 50 | o[decode(pair[0])] = decode(pair[1]); 51 | } 52 | } 53 | return o; 54 | }; 55 | 56 | var params = parseQueryString(location.search.substring(1), true); 57 | 58 | var cancelEvent = function(e) { 59 | e.stopPropagation(); 60 | e.preventDefault(); 61 | e.stopImmediatePropagation(); 62 | return false; 63 | } 64 | 65 | var disableInput = function() { 66 | document.addEventListener('click', cancelEvent); 67 | document.addEventListener('contextmenu', cancelEvent); 68 | }; 69 | 70 | disableInput(); 71 | 72 | var reenableInput = function() { 73 | document.removeEventListener('click', cancelEvent); 74 | document.removeEventListener('contextmenu', cancelEvent); 75 | } 76 | 77 | var delayedTests = []; 78 | var results = {}; 79 | 80 | var progressMessage = document.getElementById('progressMessage'); 81 | 82 | var info = function(test, text) { 83 | if (!test.resultPresented) { 84 | test.resultPresented = true; 85 | test.infoCell.textContent = text; 86 | runNextTest(test); 87 | } 88 | }; 89 | 90 | var pass = function(test, text) { 91 | if (!test.resultPresented) { 92 | test.resultPresented = true; 93 | test.resultCell.className += ' passed'; 94 | test.resultCell.textContent = '✓'; 95 | test.infoCell.textContent = text || ''; 96 | runNextTest(test); 97 | } 98 | }; 99 | var fail = function(test, text) { 100 | if (!test.resultPresented) { 101 | test.resultPresented = true; 102 | test.resultCell.className += ' failed'; 103 | test.resultCell.textContent = '✗'; 104 | test.infoCell.textContent = text || ''; 105 | testMode = TEST_MODES.ABORT; 106 | progressMessage.textContent = 'Test failed.'; 107 | reenableInput(); 108 | } 109 | }; 110 | var error = function(test, text) { 111 | if (!test.resultPresented) { 112 | test.resultPresented = true; 113 | test.resultCell.className += ' testError'; 114 | test.resultCell.textContent = '✗'; 115 | test.infoCell.textContent = text || 'test error'; 116 | testMode = TEST_MODES.ABORT; 117 | progressMessage.textContent = 'Test failed.'; 118 | reenableInput(); 119 | } 120 | }; 121 | var totalScore = 0; 122 | var totalPossibleScore = 0; 123 | var addScore = function(value, good, bad, weight, name) { 124 | var score = (value - bad) / (good - bad); 125 | if (score > 1) score = 1; 126 | if (score < 0) score = 0; 127 | totalScore += score * weight; 128 | totalPossibleScore += weight; 129 | results[name] = value; 130 | results['total'] = ((totalScore/totalPossibleScore) * 10); 131 | } 132 | 133 | var checkName = function() { 134 | if (!this.toCheck) 135 | return error(this); 136 | if (this.hasOwnProperty('on') && !this.on) 137 | return fail(this); 138 | var on = this.on || window; 139 | var toCheck = this.toCheck; 140 | if (getPrefixed(toCheck, on) !== undefined) 141 | return pass(this); 142 | else 143 | return fail(this); 144 | }; 145 | 146 | var checkWebP = function() { 147 | var image = new Image(); 148 | var that = this; 149 | image.onload = image.onerror = function() { 150 | if (image.height == 1) { 151 | pass(that); 152 | } else { 153 | fail(that); 154 | } 155 | } 156 | image.src = 'data:image/webp;base64,UklGRjAAAABXRUJQVlA4ICQAAACyAgCdASoBAAEAL/3+/3+pqampqYH8SygABc6zbAAA/lAAAAA='; 157 | }; 158 | 159 | var checkGLExtension = function() { 160 | if (!gl) 161 | return fail(this); 162 | if (!this.toCheck) 163 | return error(this); 164 | var prefixes = ['', 'WEBKIT_', 'MOZ_', 'O_', 'MS_']; 165 | for (var i = 0; i < prefixes.length; i++) { 166 | if (gl.getExtension(prefixes[i] + this.toCheck)) { 167 | return pass(this); 168 | } 169 | } 170 | return fail(this); 171 | }; 172 | 173 | var worker = null; 174 | var workerHandlers = {}; 175 | if (window.Worker) { 176 | worker = new Worker('worker.js'); 177 | worker.onmessage = function(e) { 178 | var test = e.data.test; 179 | if (!workerHandlers[test]) 180 | console.error('unhandled worker message type: ' + test); 181 | else 182 | workerHandlers[test](e); 183 | } 184 | } 185 | var spinDone = true; 186 | workerHandlers.spin = function(e) { 187 | spinDone = true; 188 | }; 189 | 190 | // We don't use getPrefixed here because we want to replace the unprefixed version with the prefixed one if it exists. 191 | if (worker) { 192 | worker.postMessage = worker.webkitPostMessage || worker.mozPostMessage || worker.oPostMessage ||worker.msPostMessage || worker.postMessage; 193 | } 194 | 195 | var checkTransferables = function() { 196 | if (!worker || !window.ArrayBuffer) 197 | return fail(this); 198 | var buffer = new Float32Array([3, 4, 5]).buffer; 199 | var test = this; 200 | worker.onmessage = function(e) { 201 | var newBuffer = e.data.buffer; 202 | if (buffer != newBuffer && buffer.byteLength == 0 && newBuffer.byteLength == 12) 203 | return pass(test); 204 | return fail(test); 205 | }; 206 | try { 207 | worker.postMessage({test: 'transferables', buffer: buffer}, [buffer]); 208 | } catch(e) { 209 | fail(this); 210 | } 211 | }; 212 | 213 | var checkWorkerGC = function() { 214 | if (!worker) { 215 | return fail(this); 216 | } 217 | } 218 | 219 | var checkWorkerString = function() { 220 | var url = getPrefixed('URL', window); 221 | if (!url || !window.Worker || !window.ArrayBuffer || !window.ArrayBuffer || !window.Blob) 222 | return fail(this); 223 | var workerSource = ''; 224 | var worker = null; 225 | try { 226 | var blob = new Blob([workerSource], { type: 'text/javascript' }); 227 | var sourceUrl = url.createObjectURL(blob); 228 | worker = new Worker(sourceUrl); 229 | } catch(e) { 230 | return fail(this); 231 | } 232 | return pass(this); 233 | } 234 | 235 | var round = function(num, sigFigs) { 236 | if (num == 0) 237 | return (0).toFixed(sigFigs - 1); 238 | var digits = Math.floor(Math.log(num) / Math.LN10) + 1; 239 | var factor = Math.pow(10, digits - sigFigs); 240 | var digitsAfterDecimal = Math.max(0, -digits + sigFigs); 241 | return (Math.round(num / factor) * factor).toFixed(digitsAfterDecimal); 242 | }; 243 | 244 | var requestServerTest = function(test, start, finish) { 245 | var request = new XMLHttpRequest(); 246 | request.open('GET', 'http://localhost:5578/test?magicPattern=' + magicPatternHex, true); 247 | request.onreadystatechange = function() { 248 | if (request.readyState == 4) { 249 | if (request.status == 200) { 250 | finish(JSON.parse(request.response)); 251 | } else if (request.status == 500) { 252 | error(test, request.response); 253 | } else { 254 | fail(test, 'Couldn\'t contact test server.'); 255 | } 256 | } 257 | }; 258 | // Wait for the test pattern with its testMode value to be drawn to the screen. 259 | setTimeout(function() { 260 | request.send(); 261 | setTimeout(start, 100); 262 | }, 200); 263 | }; 264 | 265 | var inputLatency = function() { 266 | var test = this; 267 | testMode = TEST_MODES.JAVASCRIPT_LATENCY; 268 | requestServerTest(test, function() {}, function(response) { 269 | var frames = response.keyDownLatencyMs/(1000/60); 270 | addScore(frames, 0.5, 3, 1, 'Keydown Latency'); 271 | pass(test, frames.toFixed(1) + ' frames latency (lower is better)'); 272 | }); 273 | }; 274 | 275 | var scrollLatency = function() { 276 | var test = this; 277 | testMode = TEST_MODES.SCROLL_LATENCY; 278 | requestServerTest(test, function() {}, function(response) { 279 | var frames = response.scrollLatencyMs/(1000/60); 280 | addScore(frames, 0.5, 3, 1, 'Scroll Latency'); 281 | pass(test, frames.toFixed(1) + ' frames latency (lower is better)'); 282 | }); 283 | }; 284 | 285 | var testJank = function() { 286 | var test = this; 287 | var values = []; 288 | testMode = TEST_MODES.PAUSE_TIME; 289 | if (test.testModeOverride) { 290 | testMode = test.testModeOverride; 291 | } 292 | requestServerTest(test, function() { 293 | test.iteration = 0; 294 | test.finishedMeasuring = false; 295 | test.initialized = false; 296 | test.blocker(); 297 | test.initialized = true; 298 | var callback = function() { 299 | if (test.finished) 300 | return; 301 | raf(callback); 302 | test.iteration++; 303 | test.blocker(); 304 | if (test.finishedMeasuring) { 305 | testMode = TEST_MODES.PAUSE_TIME_TEST_FINISHED; // End the test. 306 | } 307 | }; 308 | raf(callback); 309 | }, function(response) { 310 | var reports = []; 311 | for (var i = 0; i < test.report.length; i++) { 312 | switch (test.report[i]) { 313 | case 'css': 314 | var jank = response.maxCssPauseTimeMs/(1000/60); 315 | addScore(jank, 1, 5, .3, test.name + ' - CSS'); 316 | reports.push('CSS: ' + jank.toFixed(1) + ' frames jank'); 317 | break; 318 | case 'js': 319 | var jank = response.maxJSPauseTimeMs/(1000/60); 320 | addScore(jank, 1, 5, .3, test.name + ' - Javascript'); 321 | reports.push('JavaScript: ' + jank.toFixed(1) + ' frames jank'); 322 | break; 323 | case 'scroll': 324 | var jank = response.maxScrollPauseTimeMs/(1000/60); 325 | addScore(jank, 1, 5, .3, test.name + ' - Scrolling'); 326 | reports.push('Scrolling: ' + jank.toFixed(1) + ' frames jank'); 327 | break; 328 | } 329 | } 330 | pass(test, reports.join(', ') + ' (lower is better)'); 331 | }); 332 | }; 333 | 334 | 335 | var testNative = function() { 336 | var test = this; 337 | testMode = TEST_MODES.NATIVE_REFERENCE; 338 | requestServerTest(test, function() {}, function(response) { 339 | results['Native Reference - frames latency'] = (response.keyDownLatencyMs/(1000/60)).toFixed(1); 340 | results['Native Reference - frames jank'] = (response.maxCssPauseTimeMs/(1000/60)).toFixed(1); 341 | pass(test, ((response.keyDownLatencyMs/(1000/60)).toFixed(1)) + ' frames latency, ' + (response.maxCssPauseTimeMs/(1000/60)).toFixed(1) + ' frames jank (lower is better)'); 342 | }); 343 | }; 344 | 345 | 346 | var giantImageContainer = document.createElement('div'); 347 | var giantImages = []; 348 | for (var i = 0; i < 100; i++) { 349 | var giantImage = document.createElement('img'); 350 | giantImageContainer.appendChild(giantImage); 351 | giantImage.style.width = '1px' 352 | giantImage.style.height = '1px'; 353 | giantImage.done = false; 354 | giantImage.failed = false; 355 | giantImage.onload = function() { 356 | this.done = true; 357 | }.bind(giantImage); 358 | giantImage.onerror = function() { 359 | this.done = true; 360 | this.failed = true; 361 | }.bind(giantImage); 362 | giantImages.push(giantImage); 363 | } 364 | giantImageContainer.style.position = 'fixed'; 365 | giantImageContainer.style.top = '5px'; 366 | giantImageContainer.style.left = '5px'; 367 | giantImageContainer.style.zIndex = 1; 368 | // Ideally we'd use more hosts, but OS X only has one loopback address by 369 | // default. 370 | // TODO: Figure out a way to add more hosts to this test on OS X to get more 371 | // images loading concurrently. 372 | var hosts = ['http://localhost']; 373 | document.body.appendChild(giantImageContainer); 374 | // TODO: Detect unreported failed image loads (Mac Chrome) with screenshotting. 375 | var loadGiantImage = function() { 376 | var test = this; 377 | if (test.iteration == 10) { 378 | for (var i = 0; i < giantImages.length; i++) { 379 | // Use a random number for each request to defeat caching. Change hosts for each image to defeat HTTP request throttling. 380 | giantImages[i].src = hosts[i % hosts.length] + ':5578/2048.png?randomNumber=' + Math.random(); 381 | } 382 | } 383 | var done = true; 384 | for (var i = 0; i < giantImages.length; i++) { 385 | if (!giantImages[i].done) { 386 | done = false; 387 | break; 388 | } 389 | } 390 | if (done) { 391 | test.finishedMeasuring = true; 392 | for (var i = 0; i < giantImages.length; i++) { 393 | if (giantImages[i].failed) { 394 | error(test, 'Image failed to load.'); 395 | return; 396 | } 397 | } 398 | try { 399 | document.body.removeChild(giantImageContainer); 400 | } catch (ex) { 401 | // on error, we flood the console with message 402 | } 403 | } 404 | }; 405 | 406 | var control = function() { 407 | var test = this; 408 | if (test.iteration == 180) { 409 | test.finishedMeasuring = true; 410 | } 411 | }; 412 | 413 | var Tree = function(levels) { 414 | this.left = levels > 0 ? new Tree(levels - 1) : null; 415 | this.right = levels > 0 ? new Tree(levels - 1) : null; 416 | }; 417 | 418 | var giantTree = null; 419 | var smallTree = null; 420 | var gcLoad = function() { 421 | var test = this; 422 | if (!test.initialized) { 423 | // Allocate tons of long-lived memory to make subsequent GCs slow. 424 | giantTree = new Tree(22); 425 | if (worker) { 426 | spinDone = false; 427 | worker.postMessage({test: 'spin', lengthMs: 20000}); 428 | } 429 | } else { 430 | var start = getMs(); 431 | smallTree = new Tree(14); 432 | test.value = getMs() - start; 433 | } 434 | var done = false; 435 | if (worker) 436 | done = spinDone; 437 | else 438 | done = test.iteration > 240; 439 | if (done) { 440 | giantTree = null; 441 | smallTree = null; 442 | // TODO: clenaup by causing GC to happen one last time 443 | test.finishedMeasuring = true; 444 | } 445 | }; 446 | 447 | var cpuLoad = function() { 448 | var test = this; 449 | if (test.iteration > 2) { 450 | test.finishedMeasuring = true; 451 | return; 452 | } 453 | var start = getMs(); 454 | while (getMs() - start < 1000); 455 | }; 456 | 457 | var element = document.documentElement; 458 | var style = element.style; 459 | var table = document.getElementById('tests'); 460 | var tests = [ 461 | { name: 'Keydown latency', 462 | info: 'Tests the delay from keypress to on-screen response.', 463 | test: inputLatency }, 464 | { name: 'Scroll latency', 465 | info: 'Tests the delay from mousewheel movement to on-screen response.', 466 | test: scrollLatency }, 467 | { name: 'Native reference', 468 | info: 'Tests the input latency of a native app\'s window for comparison to the browser.', 469 | test: testNative }, 470 | { name: 'Baseline jank', 471 | info: 'Tests responsiveness while the browser is idle.', 472 | test: testJank, blocker: control, report: ['css', 'js', 'scroll'] }, 473 | { name: 'JavaScript jank', 474 | info: 'Tests responsiveness during JavaScript execution.', 475 | test: testJank, blocker: cpuLoad, report: ['css', 'scroll'] }, 476 | { name: 'Image loading jank', 477 | info: 'Tests responsiveness during image loading.', 478 | test: testJank, blocker: loadGiantImage, report: ['css', 'js', 'scroll'] }, 479 | 480 | // These tests work, but are disabled for now to focus on the latency test. 481 | // { name: 'requestAnimationFrame', test: checkName, toCheck: 'requestAnimationFrame' }, 482 | // { name: 'Canvas 2D', test: checkName, toCheck: 'HTMLCanvasElement' }, 483 | // { name: 'WebGL', test: checkName, toCheck: 'createShader', on: gl }, 484 | // { name: 'WebGL compressed textures', test: checkGLExtension, toCheck: 'WEBGL_compressed_texture_s3tc'}, 485 | // { name: 'WebGL floating-point textures', test: checkGLExtension, toCheck: 'OES_texture_float' }, 486 | // { name: 'WebGL depth textures', test: checkGLExtension, toCheck: 'WEBGL_depth_texture' }, 487 | // { name: 'WebGL anisotropic filtering', test: checkGLExtension, toCheck: 'EXT_texture_filter_anisotropic' }, 488 | // { name: 'WebGL standard derivatives', test: checkGLExtension, toCheck: 'OES_standard_derivatives' }, 489 | // { name: 'WebP images', test: checkWebP }, 490 | // { name: 'Typed Arrays', test: checkName, toCheck: 'ArrayBuffer' }, 491 | // { name: 'Blob', test: checkName, toCheck: 'Blob' }, 492 | // { name: 'DataView', test: checkName, toCheck: 'DataView' }, 493 | // { name: 'Web Workers', test: checkName, toCheck: 'Worker' }, 494 | // { name: 'Can create a worker from a string', test: checkWorkerString }, 495 | // { name: 'Transferable typed arrays', test: checkTransferables }, 496 | // { name: 'CSS transforms', test: checkName, toCheck: 'transform', on: style }, 497 | // { name: 'CSS animations', test: checkName, toCheck: 'animation', on: style }, 498 | // { name: 'CSS transitions', test: checkName, toCheck: 'transition', on: style }, 499 | // { name: 'CSS 3D', test: checkName, toCheck: 'perspective', on: style }, 500 | // { name: 'fullscreen', test: checkName, toCheck: 'requestFullScreen', on: element }, 501 | // { name: 'mouse lock', test: checkName, toCheck: 'requestPointerLock', on: element }, 502 | // { name: 'high-precision timers', test: checkName, toCheck: 'now', on: window.performance }, 503 | // { name: 'Web Audio', test: checkName, toCheck: 'AudioContext'}, 504 | // { name: 'Web Sockets', test: checkName, toCheck: 'WebSocket' }, 505 | // { name: 'WebRTC camera/microphone', test: checkName, toCheck: 'getUserMedia', on: navigator }, 506 | 507 | // These tests don't work yet. 508 | // { name: 'WebRTC peer connection', test: checkName, toCheck: 'PeerConnection' }, 509 | // { name: 'WebRTC peer connection binary data' }, 510 | // { name: 'WebRTC peer connection UDP' }, 511 | // { name: 'gamepad', test: checkName, toCheck: 'Gamepads', on: navigator }, 512 | // { name: 'input event timestamps' }, 513 | // { name: 'input latency with CPU load' }, 514 | // { name: 'input latency with GPU load' }, 515 | // { name: 'JavaScript doesn\'t block CSS animation' }, 516 | // { name: 'JavaScript doesn\'t block scrolling' }, 517 | // { name: 'Canvas 2D doesn\'t block JavaScript' }, 518 | // { name: 'Touch events' }, 519 | // { name: 'Device orientation' } 520 | // { name: 'GC variability', test: testJank, blocker: gcLoad }, 521 | // { name: 'Work per frame, low load', test: testJank, blocker: cpuLoad(8, 8) }, 522 | // { name: 'Work per frame, background load', test: testJank, blocker: cpuLoad(8, 8, true) }, 523 | // { name: 'Work per frame, high load', test: testJank, blocker: cpuLoad(8, 14) }, 524 | // { name: 'Worker GC doesn\'t affect main page', test: testJank, blocker: workerGCLoad }, 525 | ]; 526 | 527 | for (var i = 0; i < tests.length; i++) { 528 | var test = tests[i]; 529 | var row = document.createElement('tr'); 530 | var nameCell = document.createElement('td'); 531 | var resultCell = document.createElement('td'); 532 | var infoCell = document.createElement('td'); 533 | nameCell.className = 'testName'; 534 | nameCell.textContent = test.name; 535 | var description = document.createElement('div'); 536 | description.className = 'testDescription'; 537 | nameCell.appendChild(description); 538 | if (test.info) { 539 | description.textContent = test.info; 540 | } 541 | resultCell.className = 'testResult'; 542 | test.resultCell = document.createElement('div'); 543 | resultCell.appendChild(test.resultCell); 544 | infoCell.className = 'testInfo'; 545 | test.infoCell = infoCell; 546 | row.appendChild(nameCell); 547 | row.appendChild(resultCell); 548 | row.appendChild(infoCell); 549 | table.appendChild(row); 550 | } 551 | 552 | var nextTestIndex = 0; 553 | 554 | var runNextTest = function(previousTest) { 555 | scrollTo(0, 0); 556 | if (previousTest && !previousTest.timedOut) { 557 | if (previousTest.finished) { 558 | previousTest.infoCell.textContent = "Error: test sent multiple results."; 559 | return; 560 | } 561 | previousTest.finished = true; 562 | if (previousTest != tests[nextTestIndex - 1]) { 563 | previousTest.infoCell.textContent = "Error: test sent results out of order"; 564 | return; 565 | } 566 | } 567 | var testIndex = nextTestIndex++; 568 | if (testIndex >= tests.length) { 569 | // All tests successfully completed. Report the overall score as a number out of 10. 570 | var scoreRatio = totalScore / totalPossibleScore; 571 | var score = document.getElementById('score'); 572 | score.textContent = (scoreRatio * 10).toFixed(1); 573 | // Make the score red for low values, green for high values. 574 | score.style.color = 'hsl(' + (Math.pow(scoreRatio, 3) * 120) + ', 100%, 50%)'; 575 | progressMessage.style.display = 'none'; 576 | doneMessage.style.display = 'block'; 577 | reenableInput(); 578 | if (params.auto == 1) { 579 | if (params.results) { 580 | var xhr = new XMLHttpRequest(); 581 | xhr.open('POST', params.results, true); 582 | xhr.onreadystatechange = function() { 583 | if (xhr.readyState >= 4) { 584 | // Navigate to a different page so we stop keeping the server alive. 585 | window.location.href = 'about:blank'; 586 | } 587 | } 588 | xhr.send(JSON.stringify(results, undefined, 2)); 589 | } 590 | } 591 | // End the test run. 592 | return; 593 | } 594 | var test = tests[testIndex]; 595 | test.infoCell.textContent = ''; 596 | test.resultCell.textContent = '⋯'; 597 | setTimeout(function() { checkTimeout(test); }, 80000); 598 | if (test.test) { 599 | setTimeout(function() { 600 | try { 601 | test.test(); 602 | } catch(e) { 603 | error(test, 'unhandled exception'); 604 | } 605 | }, 1); 606 | } else { 607 | info(test, 'test not implemented'); 608 | } 609 | }; 610 | setTimeout(runNextTest, 100); 611 | 612 | var checkTimeout = function(test) { 613 | if (!test.finished) { 614 | test.timedOut = true; 615 | test.finished = true; 616 | return error(test, "Timed out"); 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /html/worker.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | self.postMessage = self.webkitPostMessage || self.mozPostMessage || self.oPostMessage || self.msPostMessage || self.postMessage; 18 | var getMs = function() { 19 | return new Date().getTime(); 20 | } 21 | self.onmessage = function(e) { 22 | if (e.data.test == 'transferables') { 23 | self.postMessage(e.data, [e.data.buffer]); 24 | } else if (e.data.test == 'spin') { 25 | var start = getMs(); 26 | while (getMs() - e.data.lengthMs < start); 27 | self.postMessage(e.data); 28 | } else { 29 | console.error('unknown worker message: ' + e.data.test); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /latency-benchmark.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'latency-benchmark', 5 | 'type': 'executable', 6 | 'sources': [ 7 | 'src/latency-benchmark.c', 8 | 'src/latency-benchmark.h', 9 | 'src/screenscraper.h', 10 | 'src/server.c', 11 | 'src/oculus.cpp', 12 | 'src/oculus.h', 13 | 'src/clioptions.c', 14 | 'src/clioptions.h', 15 | '<(INTERMEDIATE_DIR)/packaged-html-files.c', 16 | ], 17 | 'dependencies': [ 18 | 'mongoose', 19 | 'libovr', 20 | ], 21 | 'actions': [ 22 | { 23 | 'action_name': 'package html files', 24 | 'inputs': [ 25 | 'files-to-c-arrays.py', 26 | 'html/2048.png', 27 | 'html/compatibility.js', 28 | 'html/draw-pattern.js', 29 | 'html/gradient.png', 30 | 'html/hardware-latency-test.html', 31 | 'html/index.html', 32 | 'html/keep-server-alive.js', 33 | 'html/latency-benchmark.css', 34 | 'html/latency-benchmark.html', 35 | 'html/latency-benchmark.js', 36 | 'html/worker.js', 37 | ], 38 | 'outputs': [ 39 | '<(INTERMEDIATE_DIR)/packaged-html-files.c', 40 | ], 41 | 'action': ['python', 'files-to-c-arrays.py', '<@(_outputs)', '<@(_inputs)'], 42 | 'msvs_cygwin_shell': 0, 43 | }, 44 | ], 45 | 'conditions': [ 46 | ['OS=="linux"', { 47 | 'sources': [ 48 | 'src/x11/screenscraper.c', 49 | 'src/x11/main.c', 50 | ], 51 | }], 52 | ['OS=="win"', { 53 | 'sources': [ 54 | 'src/win/getopt.c', 55 | 'src/win/main.cpp', 56 | 'src/win/screenscraper.cpp', 57 | 'src/win/stdafx.h', 58 | ], 59 | }], 60 | ['OS=="mac"', { 61 | 'sources': [ 62 | 'src/mac/main.m', 63 | 'src/mac/screenscraper.m', 64 | ], 65 | 'link_settings': { 66 | 'libraries': [ 67 | '$(SDKROOT)/System/Library/Frameworks/Cocoa.framework', 68 | '$(SDKROOT)/System/Library/Frameworks/CoreVideo.framework', 69 | '$(SDKROOT)/System/Library/Frameworks/IOKit.framework', 70 | '$(SDKROOT)/System/Library/Frameworks/OpenGL.framework', 71 | ], 72 | }, 73 | }], 74 | ], 75 | 'msvs_settings': { 76 | 'VCCLCompilerTool': { 77 | 'CompileAs': 2, # Compile C as C++, since msvs doesn't support C99 78 | }, 79 | 'VCLinkerTool': { 80 | 'AdditionalDependencies': [ 81 | 'winmm.lib', 82 | 'setupapi.lib', 83 | ], 84 | }, 85 | }, 86 | }, 87 | { 88 | 'target_name': 'mongoose', 89 | 'type': 'static_library', 90 | 'sources': [ 91 | 'third_party/mongoose/mongoose.c', 92 | 'third_party/mongoose/mongoose.h', 93 | ], 94 | }, 95 | { 96 | 'target_name': 'libovr', 97 | 'type': 'static_library', 98 | 'sources': [ 99 | 'third_party/LibOVR/Include/OVR.h', 100 | 'third_party/LibOVR/Include/OVRVersion.h', 101 | 'third_party/LibOVR/Src/Kernel/OVR_Alg.cpp', 102 | 'third_party/LibOVR/Src/Kernel/OVR_Alg.h', 103 | 'third_party/LibOVR/Src/Kernel/OVR_Allocator.cpp', 104 | 'third_party/LibOVR/Src/Kernel/OVR_Allocator.h', 105 | 'third_party/LibOVR/Src/Kernel/OVR_Array.h', 106 | 'third_party/LibOVR/Src/Kernel/OVR_Atomic.cpp', 107 | 'third_party/LibOVR/Src/Kernel/OVR_Atomic.h', 108 | 'third_party/LibOVR/Src/Kernel/OVR_Color.h', 109 | 'third_party/LibOVR/Src/Kernel/OVR_ContainerAllocator.h', 110 | 'third_party/LibOVR/Src/Kernel/OVR_File.cpp', 111 | 'third_party/LibOVR/Src/Kernel/OVR_File.h', 112 | 'third_party/LibOVR/Src/Kernel/OVR_FileFILE.cpp', 113 | 'third_party/LibOVR/Src/Kernel/OVR_Hash.h', 114 | 'third_party/LibOVR/Src/Kernel/OVR_KeyCodes.h', 115 | 'third_party/LibOVR/Src/Kernel/OVR_List.h', 116 | 'third_party/LibOVR/Src/Kernel/OVR_Log.cpp', 117 | 'third_party/LibOVR/Src/Kernel/OVR_Log.h', 118 | 'third_party/LibOVR/Src/Kernel/OVR_Math.cpp', 119 | 'third_party/LibOVR/Src/Kernel/OVR_Math.h', 120 | 'third_party/LibOVR/Src/Kernel/OVR_RefCount.cpp', 121 | 'third_party/LibOVR/Src/Kernel/OVR_RefCount.h', 122 | 'third_party/LibOVR/Src/Kernel/OVR_Std.cpp', 123 | 'third_party/LibOVR/Src/Kernel/OVR_Std.h', 124 | 'third_party/LibOVR/Src/Kernel/OVR_String.cpp', 125 | 'third_party/LibOVR/Src/Kernel/OVR_String.h', 126 | 'third_party/LibOVR/Src/Kernel/OVR_String_FormatUtil.cpp', 127 | 'third_party/LibOVR/Src/Kernel/OVR_String_PathUtil.cpp', 128 | 'third_party/LibOVR/Src/Kernel/OVR_StringHash.h', 129 | 'third_party/LibOVR/Src/Kernel/OVR_SysFile.cpp', 130 | 'third_party/LibOVR/Src/Kernel/OVR_SysFile.h', 131 | 'third_party/LibOVR/Src/Kernel/OVR_System.cpp', 132 | 'third_party/LibOVR/Src/Kernel/OVR_System.h', 133 | 'third_party/LibOVR/Src/Kernel/OVR_Threads.h', 134 | 'third_party/LibOVR/Src/Kernel/OVR_Timer.cpp', 135 | 'third_party/LibOVR/Src/Kernel/OVR_Timer.h', 136 | 'third_party/LibOVR/Src/Kernel/OVR_Types.h', 137 | 'third_party/LibOVR/Src/Kernel/OVR_UTF8Util.cpp', 138 | 'third_party/LibOVR/Src/Kernel/OVR_UTF8Util.h', 139 | 'third_party/LibOVR/Src/OVR_Device.h', 140 | 'third_party/LibOVR/Src/OVR_DeviceConstants.h', 141 | 'third_party/LibOVR/Src/OVR_DeviceHandle.cpp', 142 | 'third_party/LibOVR/Src/OVR_DeviceHandle.h', 143 | 'third_party/LibOVR/Src/OVR_DeviceImpl.cpp', 144 | 'third_party/LibOVR/Src/OVR_DeviceImpl.h', 145 | 'third_party/LibOVR/Src/OVR_DeviceMessages.h', 146 | 'third_party/LibOVR/Src/OVR_HIDDevice.h', 147 | 'third_party/LibOVR/Src/OVR_HIDDeviceBase.h', 148 | 'third_party/LibOVR/Src/OVR_HIDDeviceImpl.h', 149 | 'third_party/LibOVR/Src/OVR_JSON.cpp', 150 | 'third_party/LibOVR/Src/OVR_JSON.h', 151 | 'third_party/LibOVR/Src/OVR_LatencyTestImpl.cpp', 152 | 'third_party/LibOVR/Src/OVR_LatencyTestImpl.h', 153 | 'third_party/LibOVR/Src/OVR_Profile.cpp', 154 | 'third_party/LibOVR/Src/OVR_Profile.h', 155 | 'third_party/LibOVR/Src/OVR_SensorFilter.cpp', 156 | 'third_party/LibOVR/Src/OVR_SensorFilter.h', 157 | 'third_party/LibOVR/Src/OVR_SensorFusion.cpp', 158 | 'third_party/LibOVR/Src/OVR_SensorFusion.h', 159 | 'third_party/LibOVR/Src/OVR_SensorImpl.cpp', 160 | 'third_party/LibOVR/Src/OVR_SensorImpl.h', 161 | 'third_party/LibOVR/Src/OVR_ThreadCommandQueue.cpp', 162 | 'third_party/LibOVR/Src/OVR_ThreadCommandQueue.h', 163 | 'third_party/LibOVR/Src/Util/Util_LatencyTest.cpp', 164 | 'third_party/LibOVR/Src/Util/Util_LatencyTest.h', 165 | 'third_party/LibOVR/Src/Util/Util_Render_Stereo.cpp', 166 | 'third_party/LibOVR/Src/Util/Util_Render_Stereo.h', 167 | ], 168 | 'msvs_configuration_attributes': { 169 | # Oculus code assumes Unicode mode. 170 | 'CharacterSet': '1', 171 | }, 172 | 'conditions': [ 173 | ['OS=="linux"', { 174 | 'sources': [ 175 | 'third_party/LibOVR/Src/OVR_Linux_DeviceManager.cpp', 176 | 'third_party/LibOVR/Src/OVR_Linux_DeviceManager.h', 177 | 'third_party/LibOVR/Src/OVR_Linux_HIDDevice.cpp', 178 | 'third_party/LibOVR/Src/OVR_Linux_HIDDevice.h', 179 | 'third_party/LibOVR/Src/OVR_Linux_HMDDevice.cpp', 180 | 'third_party/LibOVR/Src/OVR_Linux_HMDDevice.h', 181 | 'third_party/LibOVR/Src/OVR_Linux_SensorDevice.cpp', 182 | 'third_party/LibOVR/Src/Kernel/OVR_ThreadsPthread.cpp', 183 | ], 184 | }], 185 | ['OS=="win"', { 186 | 'sources': [ 187 | 'third_party/LibOVR/Src/OVR_Win32_DeviceManager.cpp', 188 | 'third_party/LibOVR/Src/OVR_Win32_DeviceManager.h', 189 | 'third_party/LibOVR/Src/OVR_Win32_DeviceStatus.cpp', 190 | 'third_party/LibOVR/Src/OVR_Win32_DeviceStatus.h', 191 | 'third_party/LibOVR/Src/OVR_Win32_HIDDevice.cpp', 192 | 'third_party/LibOVR/Src/OVR_Win32_HIDDevice.h', 193 | 'third_party/LibOVR/Src/OVR_Win32_HMDDevice.cpp', 194 | 'third_party/LibOVR/Src/OVR_Win32_HMDDevice.h', 195 | 'third_party/LibOVR/Src/OVR_Win32_SensorDevice.cpp', 196 | 'third_party/LibOVR/Src/OVR_Win32_SensorDevice.h', 197 | 'third_party/LibOVR/Src/Kernel/OVR_ThreadsWinAPI.cpp', 198 | ], 199 | }], 200 | ['OS=="mac"', { 201 | 'sources': [ 202 | 'third_party/LibOVR/Src/OVR_OSX_DeviceManager.cpp', 203 | 'third_party/LibOVR/Src/OVR_OSX_DeviceManager.h', 204 | 'third_party/LibOVR/Src/OVR_OSX_HIDDevice.cpp', 205 | 'third_party/LibOVR/Src/OVR_OSX_HIDDevice.h', 206 | 'third_party/LibOVR/Src/OVR_OSX_HMDDevice.cpp', 207 | 'third_party/LibOVR/Src/OVR_OSX_HMDDevice.h', 208 | 'third_party/LibOVR/Src/OVR_OSX_SensorDevice.cpp', 209 | 'third_party/LibOVR/Src/Kernel/OVR_ThreadsPthread.cpp', 210 | ], 211 | }], 212 | ], 213 | } 214 | ], 215 | 216 | 'target_defaults': { 217 | 'default_configuration': 'Debug', 218 | 'configurations': { 219 | 'Debug': { 220 | 'defines': [ 'DEBUG', '_DEBUG' ], 221 | 'cflags': [ '-g', '-O0' ], 222 | 'msvs_settings': { 223 | 'VCCLCompilerTool': { 224 | 'RuntimeLibrary': 1, # static debug 225 | 'Optimization': 0, # optimizeDisabled (/Od) 226 | }, 227 | }, 228 | 'xcode_settings': {'GCC_OPTIMIZATION_LEVEL': 0 } # -O0 No optimization 229 | }, 230 | 'Release': { 231 | 'defines': [ 'NDEBUG' ], 232 | 'cflags': [ '-Os' ], 233 | 'msvs_settings': { 234 | 'VCCLCompilerTool': { 235 | 'RuntimeLibrary': 0, # static release 236 | 'Optimization': 3, # full optimization (/Ox) 237 | }, 238 | }, 239 | 'xcode_settings': { 'GCC_OPTIMIZATION_LEVEL': 's' }, # -Os optimize for size+speed 240 | } 241 | }, 242 | 'conditions': [ 243 | ['OS=="linux"', { 244 | 'ldflags': [ '-pthread' ], 245 | 'link_settings': { 246 | 'libraries' : [ 247 | '-ldl', 248 | '-lX11', 249 | '-lXtst', 250 | '-lGL', 251 | '-ludev', 252 | '-lXinerama', 253 | ], 254 | }, 255 | }], 256 | ['OS=="win"', { 257 | 'defines': [ '_WINDOWS', 'WIN32' ], 258 | }], 259 | ['OS=="mac"', { 260 | 'xcode_settings': { 261 | 'ARCHS': '$(ARCHS_STANDARD_64_BIT)', 262 | }, 263 | }], 264 | ], 265 | 'msvs_settings': { 266 | 'VCLinkerTool': { 267 | 'GenerateDebugInformation': 'true', 268 | 'SubSystem': 2, # Windows 269 | }, 270 | }, 271 | 'xcode_settings': { 272 | # This prevents a silly warning in XCode's UI ("Update project to recommended settings"). Unfortunately there's no way to prevent the warning on 'action' targets; oh well. 273 | 'COMBINE_HIDPI_IMAGES': 'YES', 274 | }, 275 | }, 276 | 277 | 'conditions': [ 278 | ['OS=="mac"', 279 | # The XCode project generator automatically adds a bogus "All" target with bad xcode_settings unless we define one here. 280 | { 281 | 'targets': [ 282 | { 283 | 'target_name': 'All', 284 | 'type': 'static_library', 285 | }, 286 | ], 287 | }, 288 | ], 289 | ], 290 | } 291 | -------------------------------------------------------------------------------- /linux-build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$( dirname "${BASH_SOURCE[0]}" )" 3 | ./generate-project-files 4 | export CC=clang 5 | export CXX=clang++ 6 | cd build 7 | make "$@" 8 | -------------------------------------------------------------------------------- /src/clioptions.c: -------------------------------------------------------------------------------- 1 | #include "clioptions.h" 2 | #include 3 | #include 4 | #include 5 | 6 | extern char *optarg; 7 | extern int optind; 8 | extern char optopt; 9 | int getopt(int, char **, char *); 10 | 11 | void print_usage_and_exit() { 12 | fprintf(stderr, "usage: latency-benchmark -a -b path_to_browser_executable\n"); 13 | fprintf(stderr, " [-r url_to_post_results_to] [-e arguments_for_browser]\n"); 14 | fprintf(stderr, "\n"); 15 | fprintf(stderr, "Measures input latency and jank in web browsers. Specify -a, -b,\n"); 16 | fprintf(stderr, "and -r to automatically run the test and report results to a server.\n"); 17 | exit(1); 18 | } 19 | 20 | void parse_commandline(int argc, const char **argv, clioptions *options) { 21 | memset(options, 0, sizeof(*options)); 22 | 23 | // parse command line arguments 24 | int c; 25 | 26 | //TODO: use getopt_long for better looking cli args 27 | while ((c = getopt(argc, (char **)argv, "ab:d:r:e:p:h:")) != -1) { 28 | switch(c) { 29 | case 'a': 30 | options->automated = true; 31 | break; 32 | case 'b': 33 | options->browser = optarg; 34 | break; 35 | case 'r': 36 | options->results_url = optarg; 37 | break; 38 | case 'e': 39 | options->browser_args = optarg; 40 | break; 41 | case 'p': 42 | options->magic_pattern = optarg; 43 | break; 44 | case 'h': 45 | options->parent_handle = optarg; 46 | break; 47 | case ':': 48 | fprintf(stderr, "Option -%c requires an operand\n", optopt); 49 | print_usage_and_exit(); 50 | break; 51 | case '?': 52 | fprintf(stderr, "Unrecognized option: '-%c'\n", optopt); 53 | print_usage_and_exit(); 54 | } 55 | } 56 | 57 | // Validate the options. 58 | if (options->magic_pattern) { 59 | if (options->automated || options->browser || options->results_url || 60 | options->browser_args) { 61 | fprintf(stderr, "-p is incompatible with all other options except -h.\n"); 62 | print_usage_and_exit(); 63 | } 64 | } 65 | if (options->automated && !options->browser) { 66 | fprintf(stderr, "You must specify a browser executable to run in automatic mode.\n"); 67 | print_usage_and_exit(); 68 | } 69 | if (options->results_url && !options->automated) { 70 | fprintf(stderr, "Results can only be reported in automatic mode."); 71 | print_usage_and_exit(); 72 | } 73 | if (options->browser_args && !options->browser) { 74 | fprintf(stderr, "-b must be specified when -e is present."); 75 | print_usage_and_exit(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/clioptions.h: -------------------------------------------------------------------------------- 1 | #ifndef WLB_CLIOPTIONS_H_ 2 | #define WLB_CLIOPTIONS_H_ 3 | 4 | #include "screenscraper.h" 5 | 6 | typedef struct { 7 | bool automated; // is this an automated run, shutdown the browser when done 8 | char *browser; // path to the executable for the browser to launch 9 | char *browser_args; // args passed to the browser 10 | char *results_url; // URL to post results to after an automated run. 11 | char *magic_pattern; // When launching a native reference test window, this 12 | // contains the magic pattern to draw, encoded in 13 | // hexadecimal. 14 | char *parent_handle; // On Windows, this option is passed to child processes 15 | // holding the HANDLE value of their parent. 16 | } clioptions; 17 | 18 | void parse_commandline(int argc, const char **argv, clioptions *options); 19 | 20 | #endif // WLB_CLIOPTIONS_H -------------------------------------------------------------------------------- /src/latency-benchmark.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // This file contains the main platform-independent implementation of the 18 | // benchmark server. It serves the files for the test and handles all 19 | // communication with the browser test page, including sending input events, 20 | // taking screenshots, and computing statistics. 21 | 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include "screenscraper.h" 30 | #include "latency-benchmark.h" 31 | 32 | int64_t last_draw_time = 0; 33 | int64_t biggest_draw_time_gap = 0; 34 | 35 | // Updates the given pattern with the given event data, then draws the pattern 36 | // to the current OpenGL context. 37 | void draw_pattern_with_opengl(uint8_t pattern[], int scroll_events, 38 | int keydown_events, int esc_presses) { 39 | int64_t time = get_nanoseconds(); 40 | if (last_draw_time > 0) { 41 | if (time - last_draw_time > biggest_draw_time_gap) { 42 | biggest_draw_time_gap = time - last_draw_time; 43 | debug_log("New biggest draw time gap: %f ms.", 44 | biggest_draw_time_gap / (double)nanoseconds_per_millisecond); 45 | } 46 | } 47 | last_draw_time = time; 48 | if (esc_presses == 0) { 49 | pattern[4 * 4 + 2] = TEST_MODE_JAVASCRIPT_LATENCY; 50 | } else { 51 | pattern[4 * 4 + 2] = TEST_MODE_ABORT; 52 | } 53 | // Update the pattern with the number of scroll events mod 255. 54 | pattern[4 * 5] = pattern[4 * 5 + 1] = pattern[4 * 5 + 2] = scroll_events; 55 | // Update the pattern with the number of keydown events mod 255. 56 | pattern[4 * 4 + 1] = keydown_events; 57 | // Increment the "JavaScript frames" counter. 58 | pattern[4 * 4 + 0]++; 59 | // Increment the "CSS animation frames" counter. 60 | pattern[4 * 6 + 0]++; 61 | pattern[4 * 6 + 1]++; 62 | pattern[4 * 6 + 2]++; 63 | GLint viewport[4]; 64 | glGetIntegerv(GL_VIEWPORT, viewport); 65 | GLint height = viewport[3]; 66 | glDisable(GL_SCISSOR_TEST); 67 | // Alternate background each frame to make tearing easy to spot. 68 | float background = 1; 69 | if (pattern[4 * 4 + 0] % 2 == 1) 70 | background = 0.8; 71 | glClearColor(background, background, background, 1); 72 | glClear(GL_COLOR_BUFFER_BIT); 73 | glEnable(GL_SCISSOR_TEST); 74 | for (int i = 0; i < pattern_bytes; i += 4) { 75 | glClearColor(pattern[i + 2] / 255.0, 76 | pattern[i + 1] / 255.0, 77 | pattern[i] / 255.0, 1); 78 | glScissor(i / 4, height - 1, 1, 1); 79 | glClear(GL_COLOR_BUFFER_BIT); 80 | } 81 | } 82 | 83 | 84 | // Parses the magic pattern from a hexadecimal encoded string and fills 85 | // parsed_pattern with the result. parsed_pattern must be a buffer at least 86 | // pattern_magic_bytes long. 87 | bool parse_hex_magic_pattern(const char *encoded_pattern, 88 | uint8_t parsed_pattern[]) { 89 | assert(encoded_pattern); 90 | assert(parsed_pattern); 91 | if (strlen(encoded_pattern) != hex_pattern_length) { 92 | return false; 93 | } 94 | bool failed = false; 95 | for (int i = 0; i < pattern_magic_bytes; i++) { 96 | // Read the pattern from hex. Every fourth byte is the alpha channel 97 | // with an expected value of 255. 98 | if (i % 4 == 3) { 99 | parsed_pattern[i] = 255; 100 | } else { 101 | int hex_index = (i - i / 4) * 2; 102 | assert(hex_index < hex_pattern_length); 103 | int current_byte; 104 | int num_parsed = sscanf(encoded_pattern + hex_index, "%2x", 105 | ¤t_byte); 106 | failed |= 1 != num_parsed; 107 | parsed_pattern[i] = current_byte; 108 | } 109 | } 110 | return !failed; 111 | } 112 | 113 | 114 | // Encodes the given magic pattern into hexadecimal. encoded_pattern must be a 115 | // buffer at least hex_pattern_length + 1 bytes long. 116 | void hex_encode_magic_pattern(const uint8_t magic_pattern[], 117 | char encoded_pattern[]) { 118 | assert(magic_pattern); 119 | assert(encoded_pattern); 120 | int written_bytes = 0; 121 | for (int i = 0; i < pattern_magic_bytes; i++) { 122 | // Skip alpha bytes. 123 | if (i % 4 == 3) continue; 124 | assert(written_bytes < hex_pattern_length - 1); 125 | snprintf(&encoded_pattern[written_bytes], 3, "%02hhX", magic_pattern[i]); 126 | written_bytes += 2; 127 | } 128 | } 129 | 130 | 131 | // This function works something like memmem, except that it expects needle to 132 | // be 4-byte aligned in haystack (since each pixel is 4 bytes) and it ignores 133 | // every fourth byte (starting with haystack[3]) because those bytes represent 134 | // alpha. 135 | static const uint8_t *find_BGRA_pixels_ignoring_alpha(const uint8_t *haystack, 136 | size_t haystack_length, const uint8_t *needle, size_t needle_length) { 137 | const uint8_t *haystack_end = haystack + haystack_length; 138 | for(const uint8_t *i = haystack; i < haystack_end - needle_length; i += 4) { 139 | for(size_t j = 0; j < needle_length; j++) { 140 | if (j % 4 == 3 /* alpha byte */ || 141 | i[j] == needle[j] /* non-alpha byte */) { 142 | if (j == needle_length - 1) return i; 143 | } else { 144 | break; 145 | } 146 | } 147 | } 148 | return NULL; 149 | } 150 | 151 | 152 | // Locates the given pattern in the screenshot. 153 | static bool find_pattern(const uint8_t magic_pattern[], screenshot *screenshot, 154 | size_t *out_x, size_t *out_y) { 155 | assert(out_x && out_y && screenshot->width && screenshot->height); 156 | const uint8_t *found = find_BGRA_pixels_ignoring_alpha(screenshot->pixels, 157 | screenshot->stride * screenshot->height, magic_pattern, 158 | pattern_magic_bytes); 159 | if (found) { 160 | size_t offset = (size_t)(found - screenshot->pixels); 161 | *out_x = (offset % screenshot->stride) / 4; 162 | *out_y = offset / screenshot->stride; 163 | return true; 164 | } 165 | return false; 166 | } 167 | 168 | // This struct holds the data communicated from the test page to the server in 169 | // the test pattern. 170 | typedef struct { 171 | int64_t screenshot_time; 172 | uint8_t javascript_frames; 173 | uint8_t key_down_events; 174 | uint8_t css_frames; 175 | uint8_t scroll_position; 176 | test_mode_t test_mode; 177 | } measurement_t; 178 | 179 | // This function takes a small screenshot at the specified position, checks for 180 | // the magic pattern, and then fills in the measurement struct with data 181 | // decoded from the pixels of the pattern. Returns true if successful, false 182 | // if the screenshot failed or the magic pattern was not present. 183 | static bool read_data_from_screen(uint32_t x, uint32_t y, 184 | const uint8_t magic_pattern[], measurement_t *out) { 185 | assert(out); 186 | screenshot *screenshot = take_screenshot(x, y, pattern_pixels, 1); 187 | if (!screenshot) { 188 | return false; 189 | } 190 | if (screenshot->width != pattern_pixels) { 191 | free_screenshot(screenshot); 192 | return false; 193 | } 194 | // Check that the magic pattern is there, starting at the first pixel. 195 | size_t found_x, found_y; 196 | if (!find_pattern(magic_pattern, screenshot, &found_x, &found_y) || 197 | found_x || found_y) { 198 | free_screenshot(screenshot); 199 | return false; 200 | } 201 | out->javascript_frames = screenshot->pixels[pattern_magic_pixels * 4 + 0]; 202 | out->key_down_events = screenshot->pixels[pattern_magic_pixels * 4 + 1]; 203 | out->test_mode = (test_mode_t) screenshot->pixels[pattern_magic_pixels * 4 + 2]; 204 | out->scroll_position = screenshot->pixels[(pattern_magic_pixels + 1) * 4]; 205 | out->css_frames = screenshot->pixels[(pattern_magic_pixels + 2) * 4]; 206 | out->screenshot_time = screenshot->time_nanoseconds; 207 | free_screenshot(screenshot); 208 | debug_log("javascript frames: %d, javascript events: %d, scroll position: %d" 209 | ", css frames: %d, test mode: %d", out->javascript_frames, 210 | out->key_down_events, out->scroll_position, out->css_frames, 211 | out->test_mode); 212 | return true; 213 | } 214 | 215 | // Each value reported in the measurement struct is tracked by a statistic 216 | // struct that records the length of time between changes. 217 | typedef struct { 218 | int64_t previous_change_time; // The last time the value changed. 219 | int value; // The last value seen. 220 | int value_delta; // The amount the value changed last time. 221 | int measurements; // The number of measurements taken. 222 | // We want to know the amount of time it took the value to change, but we 223 | // can't take screenshots fast enough to pin it down exactly. When we see a 224 | // screenshot with a changed value, we can only tell that the value changed 225 | // sometime during the period between the current screenshot and the previous 226 | // one. This period may be tens of milliseconds. To deal with this we record 227 | // two times: the time of the previous screenshot, and the time of the current 228 | // one. These correspond to a lower and upper bound on the time when the value 229 | // actually changed. Ideally, screenshots will be frequent enough that the 230 | // difference between these times is small. 231 | // These variables hold the sum of the time for all measurements; divide by 232 | // the number of measurements to get the average time. 233 | int64_t lower_bound_time; 234 | int64_t upper_bound_time; 235 | // This records the longest length of time during which the value did not 236 | // change. 237 | int64_t max_lower_bound; 238 | char *name; 239 | } statistic; 240 | 241 | 242 | // Updates a statistic struct with a new value from a recent measurement. 243 | static bool update_statistic(statistic *stat, int value, int64_t screenshot_time, 244 | int64_t previous_screenshot_time) { 245 | assert(value >= 0 && stat->value >= 0); 246 | int change = value - stat->value; 247 | if (change < 0) { 248 | // Handle values that wrap at 255. 249 | assert(stat->value < 256 && value < 256); 250 | change += 256; 251 | assert(change > 0); 252 | } 253 | assert(change >= 0); 254 | if (change == 0) { 255 | return false; 256 | } 257 | int64_t lower_bound_time = previous_screenshot_time - stat->previous_change_time; 258 | int64_t screenshot_duration = screenshot_time - previous_screenshot_time; 259 | if (lower_bound_time <= 0) { 260 | debug_log("%s: Didn't get a screenshot before response.", stat->name); 261 | } else if (screenshot_duration > 20 * nanoseconds_per_millisecond && 262 | lower_bound_time < 5 * nanoseconds_per_millisecond) { 263 | debug_log("%s: Ignoring measurement due to slow screenshot.", stat->name); 264 | } else { 265 | // Record the measurement. 266 | stat->measurements++; 267 | stat->upper_bound_time += screenshot_time - stat->previous_change_time; 268 | stat->lower_bound_time += lower_bound_time; 269 | if (lower_bound_time > stat->max_lower_bound) { 270 | debug_log("%s: updated max_lower_bound to %f", stat->name, 271 | lower_bound_time / (double)nanoseconds_per_millisecond); 272 | stat->max_lower_bound = lower_bound_time; 273 | } 274 | } 275 | stat->previous_change_time = screenshot_time; 276 | stat->value = value; 277 | stat->value_delta += change; 278 | return true; 279 | } 280 | 281 | // Returns the average upper bound time for a statistic, in milliseconds. 282 | static double upper_bound_ms(statistic *stat) { 283 | double bound = stat->upper_bound_time / (double) stat->measurements / 284 | nanoseconds_per_millisecond; 285 | // Guard for NaN resulting from divide-by-zero. 286 | if (bound != bound) 287 | bound = 0; 288 | return bound; 289 | } 290 | 291 | // Returns the average upper bound time for a statistic, in milliseconds. 292 | static double lower_bound_ms(statistic *stat) { 293 | double bound = stat->lower_bound_time / (double) stat->measurements / 294 | nanoseconds_per_millisecond; 295 | // Guard for NaN resulting from divide-by-zero. 296 | if (bound != bound) 297 | bound = 0; 298 | return bound; 299 | } 300 | 301 | // Initializes a statistic struct. 302 | static void init_statistic(char *name, statistic *stat, int value, 303 | int64_t start_time) { 304 | memset(stat, 0, sizeof(statistic)); 305 | stat->value = value; 306 | stat->previous_change_time = start_time; 307 | stat->name = name; 308 | } 309 | 310 | 311 | static const int64_t test_timeout_ms = 80000; 312 | static const int64_t event_response_timeout_ms = 4000; 313 | static const int latency_measurements_to_take = 50; 314 | 315 | // Main test function. Locates the given magic pixel pattern on the screen, then 316 | // runs one full latency test, sending input events and recording responses. On 317 | // success, the results of the test are reported in the output parameters, and 318 | // true is returned. If the test fails, the error parameter is filled in with 319 | // an error message and false is returned. 320 | bool measure_latency( 321 | const uint8_t magic_pattern[], 322 | double *out_key_down_latency_ms, 323 | double *out_scroll_latency_ms, 324 | double *out_max_js_pause_time_ms, 325 | double *out_max_css_pause_time_ms, 326 | double *out_max_scroll_pause_time_ms, 327 | char **error) { 328 | screenshot *screenshot = take_screenshot(0, 0, UINT32_MAX, UINT32_MAX); 329 | if (!screenshot) { 330 | *error = "Failed to take screenshot."; 331 | return false; 332 | } 333 | assert(screenshot->width > 0 && screenshot->height > 0); 334 | 335 | size_t x, y; 336 | bool found_pattern = find_pattern(magic_pattern, screenshot, &x, &y); 337 | free_screenshot(screenshot); 338 | if (!found_pattern) { 339 | *error = "Failed to find test pattern on screen. Ensure that your browser's zoom level is set to \"100%\", and the top-left corner of the window is visible. If you have multiple displays, try moving the browser window to the main display."; 340 | return false; 341 | } 342 | uint8_t full_pattern[pattern_bytes]; 343 | for (int i = 0; i < pattern_magic_bytes; i++) { 344 | full_pattern[i] = magic_pattern[i]; 345 | } 346 | measurement_t measurement; 347 | measurement_t previous_measurement; 348 | memset(&measurement, 0, sizeof(measurement_t)); 349 | memset(&previous_measurement, 0, sizeof(measurement_t)); 350 | int screenshots = 0; 351 | bool first_screenshot_successful = read_data_from_screen((uint32_t)x, 352 | (uint32_t) y, magic_pattern, &measurement); 353 | if (!first_screenshot_successful) { 354 | *error = "Failed to read data from test pattern."; 355 | return false; 356 | } 357 | if (measurement.test_mode == TEST_MODE_NATIVE_REFERENCE) { 358 | uint8_t *test_pattern = (uint8_t *)malloc(pattern_bytes); 359 | memset(test_pattern, 0, pattern_bytes); 360 | for (int i = 0; i < pattern_magic_bytes; i++) { 361 | test_pattern[i] = rand(); 362 | } 363 | if (!open_native_reference_window(test_pattern)) { 364 | *error = "Failed to open native reference window."; 365 | return false; 366 | } 367 | bool return_value = measure_latency(test_pattern, out_key_down_latency_ms, out_scroll_latency_ms, out_max_js_pause_time_ms, out_max_css_pause_time_ms, out_max_scroll_pause_time_ms, error); 368 | if (!close_native_reference_window()) { 369 | debug_log("Failed to close native reference window."); 370 | }; 371 | return return_value; 372 | } 373 | int64_t start_time = measurement.screenshot_time; 374 | previous_measurement = measurement; 375 | statistic javascript_frames; 376 | statistic css_frames; 377 | statistic key_down_events; 378 | statistic scroll_stats; 379 | init_statistic("javascript_frames", &javascript_frames, 380 | measurement.javascript_frames, start_time); 381 | init_statistic("key_down_events", &key_down_events, 382 | measurement.key_down_events, start_time); 383 | init_statistic("css_frames", &css_frames, measurement.css_frames, start_time); 384 | init_statistic("scroll", &scroll_stats, measurement.scroll_position, 385 | start_time); 386 | int sent_events = 0; 387 | int scroll_x = x + 40; 388 | int scroll_y = y + 40; 389 | int64_t last_scroll_sent = start_time; 390 | if (measurement.test_mode == TEST_MODE_SCROLL_LATENCY) { 391 | send_scroll_down(scroll_x, scroll_y); 392 | scroll_stats.previous_change_time = get_nanoseconds(); 393 | } 394 | while(true) { 395 | bool screenshot_successful = read_data_from_screen((uint32_t)x, 396 | (uint32_t) y, magic_pattern, &measurement); 397 | if (!screenshot_successful) { 398 | *error = "Test window moved during test. The test window must remain " 399 | "stationary and focused during the entire test."; 400 | return false; 401 | } 402 | if (measurement.test_mode == TEST_MODE_ABORT) { 403 | *error = "Test aborted."; 404 | return false; 405 | } 406 | screenshots++; 407 | int64_t screenshot_time = measurement.screenshot_time; 408 | int64_t previous_screenshot_time = previous_measurement.screenshot_time; 409 | debug_log("screenshot time %f", 410 | (screenshot_time - previous_screenshot_time) / 411 | (double)nanoseconds_per_millisecond); 412 | update_statistic(&javascript_frames, measurement.javascript_frames, 413 | screenshot_time, previous_screenshot_time); 414 | update_statistic(&key_down_events, measurement.key_down_events, 415 | screenshot_time, previous_screenshot_time); 416 | update_statistic(&css_frames, measurement.css_frames, screenshot_time, 417 | previous_screenshot_time); 418 | bool scroll_updated = update_statistic(&scroll_stats, 419 | measurement.scroll_position, screenshot_time, previous_screenshot_time); 420 | 421 | if (measurement.test_mode == TEST_MODE_JAVASCRIPT_LATENCY) { 422 | if (key_down_events.measurements >= latency_measurements_to_take) { 423 | break; 424 | } 425 | if (key_down_events.value_delta > sent_events) { 426 | *error = "More events received than sent! This is probably a bug in " 427 | "the test."; 428 | return false; 429 | } 430 | if (screenshot_time - key_down_events.previous_change_time > 431 | event_response_timeout_ms * nanoseconds_per_millisecond) { 432 | *error = "Browser did not respond to keyboard input. Make sure the " 433 | "test page remains focused for the entire test."; 434 | return false; 435 | } 436 | if (key_down_events.value_delta == sent_events) { 437 | // We want to avoid sending input events at a predictable time relative 438 | // to frames, so introduce a random delay of up to 1 frame (16.67 ms) 439 | // before sending the next event. 440 | usleep((rand() % 17) * 1000); 441 | if (!send_keystroke_z()) { 442 | *error = "Failed to send keystroke for \"Z\" key to test window."; 443 | return false; 444 | } 445 | key_down_events.previous_change_time = get_nanoseconds(); 446 | sent_events++; 447 | } 448 | } else if (measurement.test_mode == TEST_MODE_SCROLL_LATENCY) { 449 | if (scroll_stats.measurements >= latency_measurements_to_take) { 450 | break; 451 | } 452 | if (screenshot_time - scroll_stats.previous_change_time > 453 | event_response_timeout_ms * nanoseconds_per_millisecond) { 454 | *error = "Browser did not respond to scroll events. Make sure the " 455 | "test page remains focused for the entire test."; 456 | return false; 457 | } 458 | if (scroll_updated) { 459 | debug_log("scroll measurements: %d", scroll_stats.measurements); 460 | // We saw the start of a scroll. Wait for the scroll animation to 461 | // finish before continuing. We assume the animation is finished if 462 | // it's been 100 milliseconds since we last saw the scroll position 463 | // change. 464 | int64_t scroll_update_time = screenshot_time; 465 | int64_t scroll_wait_start_time = screenshot_time; 466 | while (screenshot_time - scroll_update_time < 467 | 100 * nanoseconds_per_millisecond) { 468 | screenshot_successful = read_data_from_screen((uint32_t)x, 469 | (uint32_t) y, magic_pattern, &measurement); 470 | if (!screenshot_successful) { 471 | *error = "Test window moved during test. The test window must " 472 | "remain stationary and focused during the entire test."; 473 | return false; 474 | } 475 | screenshot_time = measurement.screenshot_time; 476 | if (screenshot_time - scroll_wait_start_time > 477 | nanoseconds_per_second) { 478 | *error = "Browser kept scrolling for more than 1 second after a " 479 | "single scrollwheel event."; 480 | return false; 481 | } 482 | if (measurement.scroll_position != scroll_stats.value) { 483 | scroll_stats.value = measurement.scroll_position; 484 | scroll_update_time = screenshot_time; 485 | } 486 | } 487 | // We want to avoid sending input events at a predictable time 488 | // relative to frames, so introduce a random delay of up to 1 frame 489 | // (16.67 ms) before sending the next event. 490 | usleep((rand() % 17) * 1000); 491 | send_scroll_down(scroll_x, scroll_y); 492 | scroll_stats.previous_change_time = get_nanoseconds(); 493 | } 494 | } else if (measurement.test_mode == TEST_MODE_PAUSE_TIME) { 495 | // For the pause time test we want the browser to scroll continuously. 496 | // Send a scroll event every frame. 497 | if (screenshot_time - last_scroll_sent > 498 | 17 * nanoseconds_per_millisecond) { 499 | send_scroll_down(scroll_x, scroll_y); 500 | last_scroll_sent = get_nanoseconds(); 501 | } 502 | } else if (measurement.test_mode == TEST_MODE_PAUSE_TIME_TEST_FINISHED) { 503 | break; 504 | } else { 505 | *error = "Invalid test type. This is a bug in the test."; 506 | return false; 507 | } 508 | 509 | if (screenshot_time - start_time > 510 | test_timeout_ms * nanoseconds_per_millisecond) { 511 | *error = "Timeout."; 512 | return false; 513 | } 514 | previous_measurement = measurement; 515 | usleep(0); 516 | } 517 | // The latency we report is the midpoint of the interval given by the average 518 | // upper and lower bounds we've computed. 519 | *out_key_down_latency_ms = 520 | (upper_bound_ms(&key_down_events) + lower_bound_ms(&key_down_events)) / 2; 521 | *out_scroll_latency_ms = 522 | (upper_bound_ms(&scroll_stats) + lower_bound_ms(&scroll_stats) / 2); 523 | *out_max_js_pause_time_ms = 524 | javascript_frames.max_lower_bound / (double) nanoseconds_per_millisecond; 525 | *out_max_css_pause_time_ms = 526 | css_frames.max_lower_bound / (double) nanoseconds_per_millisecond; 527 | *out_max_scroll_pause_time_ms = 528 | scroll_stats.max_lower_bound / (double) nanoseconds_per_millisecond; 529 | debug_log("out_key_down_latency_ms: %f out_scroll_latency_ms: %f " 530 | "out_max_js_pause_time_ms: %f out_max_css_pause_time: %f\n " 531 | "out_max_scroll_pause_time_ms: %f", 532 | *out_key_down_latency_ms, 533 | *out_scroll_latency_ms, 534 | *out_max_js_pause_time_ms, 535 | *out_max_css_pause_time_ms, 536 | *out_max_scroll_pause_time_ms); 537 | return true; 538 | } 539 | -------------------------------------------------------------------------------- /src/latency-benchmark.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef WLB_LATENCY_BENCHMARK_H_ 18 | #define WLB_LATENCY_BENCHMARK_H_ 19 | 20 | #ifdef _WINDOWS 21 | #define WIN32_LEAN_AND_MEAN 22 | #include // Required by gl.h on Windows :( 23 | #endif 24 | #ifdef __APPLE__ 25 | #include 26 | #else 27 | #include 28 | #endif 29 | 30 | // The test mode is communicated from the test page to the server as one of the 31 | // pixel values in the test pattern. 32 | typedef enum { 33 | TEST_MODE_JAVASCRIPT_LATENCY = 1, 34 | TEST_MODE_SCROLL_LATENCY = 2, 35 | TEST_MODE_PAUSE_TIME = 3, 36 | TEST_MODE_PAUSE_TIME_TEST_FINISHED = 4, 37 | TEST_MODE_NATIVE_REFERENCE = 5, 38 | TEST_MODE_ABORT = 6, 39 | } test_mode_t; 40 | 41 | // Main test function. Locates the given magic pixel pattern on the screen, then 42 | // runs one full latency test, sending input events and recording responses. On 43 | // success, the results of the test are reported in the output parameters, and 44 | // true is returned. If the test fails, the error parameter is filled in with 45 | // an error message and false is returned. 46 | bool measure_latency( 47 | const uint8_t magic_pattern[], 48 | double *out_key_down_latency_ms, 49 | double *out_scroll_latency_ms, 50 | double *out_max_js_pause_time_ms, 51 | double *out_max_css_pause_time_ms, 52 | double *out_max_scroll_pause_time_ms, 53 | char **error); 54 | 55 | // Updates the given pattern with the given event data, then draws the pattern to 56 | // the current OpenGL context. 57 | void draw_pattern_with_opengl(uint8_t pattern[], int scroll_events, 58 | int keydown_events, int esc_presses); 59 | 60 | // Parses the magic pattern from a hexadecimal encoded string and fills 61 | // parsed_pattern with the result. parsed_pattern must be a buffer at least 62 | // pattern_magic_bytes long. 63 | bool parse_hex_magic_pattern(const char *encoded_pattern, 64 | uint8_t parsed_pattern[]); 65 | 66 | // Encodes the given magic pattern into hexadecimal. encoded_pattern must be a 67 | // buffer at least hex_pattern_length + 1 bytes long. 68 | void hex_encode_magic_pattern(const uint8_t magic_pattern[], 69 | char encoded_pattern[]); 70 | 71 | #endif // WLB_LATENCY_BENCHMARK_H_ 72 | -------------------------------------------------------------------------------- /src/mac/main.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #import 18 | #import 19 | #import 20 | #include 21 | #include "../latency-benchmark.h" 22 | #include "screenscraper.h" 23 | #include "clioptions.h" 24 | 25 | 26 | NSOpenGLContext *context; 27 | uint8_t pattern[pattern_bytes]; 28 | static int scrolls = 0; 29 | static int key_downs = 0; 30 | static int esc_presses = 0; 31 | 32 | // This callback is called for each display refresh by CVDisplayLink so that we 33 | // can draw at exactly the display's refresh rate. 34 | static CVReturn vsync_callback( 35 | CVDisplayLinkRef displayLink, 36 | const CVTimeStamp *inNow, 37 | const CVTimeStamp *inOutputTime, 38 | CVOptionFlags flagsIn, 39 | CVOptionFlags *flagsOut, 40 | void *displayLinkContext) { 41 | if (getppid() == 1) { 42 | // The parent process died, so we should exit immediately. 43 | exit(0); 44 | } 45 | 46 | // Wait until 6ms before the next frame to actually render. This helps reduce 47 | // latency. 48 | // TODO: Set realtime thread scheduling policy to get more reliable wait 49 | // times. This will allow waiting longer before starting to render without 50 | // risking a missed frame. 51 | // TODO: CVDisplayLink does adaptive framerate scheduling which is not latency 52 | // focused. Safari gets better latency than CVDisplayLink. Move rendering to 53 | // a separate thread to allow implementing more sophisticated frame scheduling 54 | // and beat Safari's latency. 55 | mach_timebase_info_data_t timebase; 56 | mach_timebase_info(&timebase); 57 | uint64_t ms_in_mach_time = 1000000 * timebase.denom / timebase.numer; 58 | mach_wait_until(inOutputTime->hostTime - ms_in_mach_time * 6); 59 | 60 | // We must lock the OpenGL context since it's shared with the main thread. 61 | CGLLockContext((CGLContextObj)[context CGLContextObj]); 62 | [context makeCurrentContext]; 63 | draw_pattern_with_opengl(pattern, scrolls, key_downs, esc_presses); 64 | [context flushBuffer]; 65 | CGLUnlockContext((CGLContextObj)[context CGLContextObj]); 66 | return kCVReturnSuccess; 67 | } 68 | 69 | // NSWindow's default implementation of canBecomeKeyWindow returns NO if the 70 | // window is borderless, which prevents the window from gaining keyboard focus. 71 | // KeyWindow overrides canBecomeKeyWindow to always be YES. 72 | @interface KeyWindow : NSWindow 73 | @end 74 | @implementation KeyWindow 75 | - (BOOL)canBecomeKeyWindow { return YES; } 76 | @end 77 | 78 | 79 | void run_server(clioptions *options); 80 | 81 | int main(int argc, const char *argv[]) 82 | { 83 | // SIGPIPE will terminate the process if we write to a closed socket, unless 84 | // we disable it like so. Note that GDB/LLDB will stop on SIGPIPE anyway 85 | // unless you configure them not to. 86 | // http://stackoverflow.com/questions/10431579/permanently-configuring-lldb-in-xcode-4-3-2-not-to-stop-on-signals 87 | signal(SIGPIPE, SIG_IGN); 88 | 89 | clioptions options; 90 | parse_commandline(argc, argv, &options); 91 | // Unless -p was specified, run the test server. 92 | if (!options.magic_pattern) { 93 | run_server(&options); 94 | exit(0); 95 | } 96 | // If -p was specified, we must be a child process of a server, and our 97 | // job is to create a reference test window. 98 | memset(pattern, 0, sizeof(pattern)); 99 | if (!parse_hex_magic_pattern(options.magic_pattern, pattern)) { 100 | debug_log("Failed to parse pattern."); 101 | return 1; 102 | } 103 | ProcessSerialNumber psn; 104 | OSErr err = GetCurrentProcess(&psn); 105 | assert(!err); 106 | // By default, naked binary applications are not allowed to create windows 107 | // and receive input focus. This call allows us to create windows that receive 108 | // input focus, but does not cause the creation of a Dock icon or menu bar. 109 | TransformProcessType(&psn, kProcessTransformToUIElementApplication); 110 | @autoreleasepool { 111 | // Initialize Cocoa. 112 | [NSApplication sharedApplication]; 113 | // Create a borderless window that can accept key window status (=input 114 | // focus). 115 | NSRect window_rect = NSMakeRect(0, 0, pattern_pixels, 1); 116 | window_rect = [[[NSScreen screens] objectAtIndex:0] convertRectFromBacking:window_rect]; 117 | // TODO: Choose window position to overlap the test pattern in the browser 118 | // window running the test, so as to appear on the same monitor. 119 | window_rect.origin = CGPointMake(500, 500); 120 | NSWindow *window = [[KeyWindow alloc] initWithContentRect:window_rect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; 121 | // Create an OpenGL context. 122 | NSOpenGLPixelFormatAttribute attrs[] = { NSOpenGLPFADoubleBuffer, 0 }; 123 | NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; 124 | context = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:nil]; 125 | [pixelFormat release]; 126 | // Disable vsync. When vsync is enabled calling [context flushBuffer] can 127 | // block for up to 1 frame, which can add unwanted latency. Actually Quartz 128 | // sometimes overrides this setting and temporarily turns vsync back on, but 129 | // there's nothing we can do about that from here. The only way to turn that 130 | // behavior off is with the "Beam Sync" setting available in Apple's "Quartz 131 | // Debug" tool, part of "Graphics Tools for XCode". 132 | GLint param = 0; 133 | CGLSetParameter([context CGLContextObj], kCGLCPSwapInterval, ¶m); 134 | // Make sure that the driver presents all frames immediately without queuing 135 | // any. 136 | param = 1; 137 | CGLSetParameter([context CGLContextObj], kCGLCPMPSwapsInFlight, ¶m); 138 | // Request a high DPI backbuffer on retina displays. 139 | [[window contentView] setWantsBestResolutionOpenGLSurface:YES]; 140 | // Attach the context to the window. 141 | [context setView:[window contentView]]; 142 | // Draw the test pattern on the window before it is shown. 143 | [context makeCurrentContext]; 144 | draw_pattern_with_opengl(pattern, scrolls, key_downs, esc_presses); 145 | [context flushBuffer]; 146 | // Show the window. 147 | [window makeKeyAndOrderFront:window]; 148 | // CVDisplayLink starts a dedicated rendering thread that receives callbacks 149 | // at each vsync interval. 150 | CVDisplayLinkRef displayLink; 151 | // TODO: Ensure the CVDisplayLink is attached to the correct monitor. 152 | CVDisplayLinkCreateWithActiveCGDisplays(&displayLink); 153 | CVDisplayLinkSetOutputCallback(displayLink, &vsync_callback, nil); 154 | CVDisplayLinkStart(displayLink); 155 | // Listen for scroll wheel and keyboard events and update the appropriate 156 | // counters (on the main UI thread). 157 | [NSEvent addLocalMonitorForEventsMatchingMask:NSScrollWheelMask handler:^NSEvent *(NSEvent *event) { 158 | scrolls++; 159 | return nil; 160 | }]; 161 | [NSEvent addLocalMonitorForEventsMatchingMask:NSKeyDownMask handler:^NSEvent *(NSEvent *event) { 162 | if ([event keyCode] == 53) { 163 | esc_presses++; 164 | } 165 | key_downs++; 166 | return nil; 167 | }]; 168 | // Steal input focus and become the topmost window. 169 | [NSApp activateIgnoringOtherApps:YES]; 170 | // Pass control to the Cocoa event loop. The parent process will kill us 171 | // when it's done testing. As a fallback, the CVDisplayLink callback will 172 | // kill the process if the parent process dies before it gets a chance to 173 | // kill us. 174 | [NSApp run]; 175 | } 176 | return 0; 177 | } 178 | -------------------------------------------------------------------------------- /src/mac/screenscraper.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #import "../screenscraper.h" 19 | #import "../latency-benchmark.h" 20 | #import 21 | #import 22 | 23 | 24 | const float float_epsilon = 0.0001; 25 | bool near_integer(float f) { 26 | return fabsf(remainderf(f, 1)) < float_epsilon; 27 | } 28 | 29 | static const CGWindowImageOption image_options = 30 | kCGWindowImageBestResolution | kCGWindowImageShouldBeOpaque; 31 | 32 | screenshot *take_screenshot(uint32_t x, uint32_t y, uint32_t width, 33 | uint32_t height) { 34 | // TODO: support multiple monitors. 35 | NSScreen *screen = [[NSScreen screens] objectAtIndex:0]; 36 | CGRect screen_rect = [screen convertRectToBacking:[screen frame]]; 37 | CGRect capture_rect = { .origin.x = x, .origin.y = y, .size.width = width, 38 | .size.height = height }; 39 | // Clamp to the screen. 40 | capture_rect = CGRectIntersection(capture_rect, screen_rect); 41 | // Convert to logical pixels from backing store pixels. 42 | CGRect converted_capture_rect = [screen convertRectFromBacking:capture_rect]; 43 | // Make sure we are at an integer logical pixel to satisfy 44 | // CGWindowListCreateImage. 45 | if (!near_integer(converted_capture_rect.origin.x) || 46 | !near_integer(converted_capture_rect.origin.y)) { 47 | debug_log( 48 | "Can't take screenshot at odd coordinates on a high DPI display."); 49 | return NULL; 50 | } 51 | // Round width/height up to the next logical pixel. 52 | converted_capture_rect.size.width = ceilf(converted_capture_rect.size.width); 53 | converted_capture_rect.size.height = 54 | ceilf(converted_capture_rect.size.height); 55 | // Update capture_rect with the final rounded values. 56 | capture_rect = [screen convertRectToBacking:converted_capture_rect]; 57 | CGImageRef window_image = CGWindowListCreateImage(converted_capture_rect, 58 | kCGWindowListOptionAll, kCGNullWindowID, image_options); 59 | int64_t screenshot_time = get_nanoseconds(); 60 | if (!window_image) { 61 | debug_log("CGWindowListCreateImage failed"); 62 | return NULL; 63 | } 64 | size_t image_width = CGImageGetWidth(window_image); 65 | size_t image_height = CGImageGetHeight(window_image); 66 | assert(image_width == capture_rect.size.width); 67 | assert(image_height == capture_rect.size.height); 68 | size_t stride = CGImageGetBytesPerRow(window_image); 69 | // Assert 32bpp BGRA pixel format. 70 | size_t bpp = CGImageGetBitsPerPixel(window_image); 71 | size_t bpc = CGImageGetBitsPerComponent(window_image); 72 | CGBitmapInfo bitmap_info = CGImageGetBitmapInfo(window_image); 73 | // I think something will probably break if we're not little endian. 74 | assert(kCGBitmapByteOrder32Little == kCGBitmapByteOrder32Host); 75 | // We expect little-endian, alpha "first", which in reality comes out to BGRA 76 | // byte order. 77 | bool correct_byte_order = 78 | (bitmap_info & kCGBitmapByteOrderMask) == kCGBitmapByteOrder32Little; 79 | bool correct_alpha_location = bitmap_info & kCGBitmapAlphaInfoMask & 80 | (kCGImageAlphaFirst | kCGImageAlphaNoneSkipFirst | 81 | kCGImageAlphaPremultipliedFirst); 82 | if (bpp != 32 || bpc != 8 || !correct_byte_order || !correct_alpha_location) { 83 | debug_log("Incorrect image format from CGWindowListCreateImage. " 84 | "bpp = %d, bpc = %d, byte order = %s, alpha location = %s", 85 | bpp, bpc, correct_byte_order ? "correct" : "wrong", 86 | correct_alpha_location ? "correct" : "wrong"); 87 | CFRelease(window_image); 88 | return NULL; 89 | } 90 | CFDataRef image_data = 91 | CGDataProviderCopyData(CGImageGetDataProvider(window_image)); 92 | CFRelease(window_image); 93 | const uint8_t *pixels = CFDataGetBytePtr(image_data); 94 | screenshot *shot = (screenshot *)malloc(sizeof(screenshot)); 95 | shot->width = (int32_t)image_width; 96 | shot->height = (int32_t)image_height; 97 | shot->stride = (int32_t)stride; 98 | shot->pixels = pixels; 99 | shot->time_nanoseconds = screenshot_time; 100 | shot->platform_specific_data = (void *)image_data; 101 | return shot; 102 | } 103 | 104 | void free_screenshot(screenshot *shot) { 105 | CFRelease((CFDataRef)shot->platform_specific_data); 106 | free(shot); 107 | } 108 | 109 | bool send_keystroke(int keyCode) { 110 | CGEventRef down = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)keyCode, true); 111 | CGEventRef up = CGEventCreateKeyboardEvent(NULL, (CGKeyCode)keyCode, false); 112 | CGEventPost(kCGHIDEventTap, down); 113 | CGEventPost(kCGHIDEventTap, up); 114 | CFRelease(down); 115 | CFRelease(up); 116 | return true; 117 | } 118 | 119 | bool send_keystroke_b() { return send_keystroke(11); } 120 | bool send_keystroke_t() { return send_keystroke(17); } 121 | bool send_keystroke_w() { return send_keystroke(13); } 122 | bool send_keystroke_z() { return send_keystroke(6); } 123 | 124 | bool send_scroll_down(x, y) { 125 | CGFloat devicePixelRatio = 126 | [[[NSScreen screens] objectAtIndex:0] backingScaleFactor]; 127 | CGWarpMouseCursorPosition( 128 | CGPointMake(x / devicePixelRatio, y / devicePixelRatio)); 129 | CGEventRef scrollEvent = CGEventCreateScrollWheelEvent(NULL, 130 | kCGScrollEventUnitPixel, 1, -20); 131 | CGEventPost(kCGHIDEventTap, scrollEvent); 132 | CFRelease(scrollEvent); 133 | return true; 134 | } 135 | 136 | static AbsoluteTime start_time = { .hi = 0, .lo = 0 }; 137 | int64_t get_nanoseconds() { 138 | // TODO: Apple deprecated UpTime(), so switch to mach_absolute_time. 139 | if (UnsignedWideToUInt64(start_time) == 0) { 140 | start_time = UpTime(); 141 | return 0; 142 | } 143 | return UnsignedWideToUInt64(AbsoluteDeltaToNanoseconds(UpTime(), start_time)); 144 | } 145 | 146 | void debug_log(const char *message, ...) { 147 | #ifdef DEBUG 148 | va_list list; 149 | va_start(list, message); 150 | vprintf(message, list); 151 | va_end(list); 152 | putchar('\n'); 153 | #endif 154 | } 155 | 156 | static pid_t browser_process_pid = 0; 157 | 158 | bool open_browser(const char *program, const char *args, const char *url) { 159 | assert(url); 160 | if (browser_process_pid) { 161 | debug_log("Warning: calling open_browser, but browser already open."); 162 | } 163 | if (program == NULL) { 164 | return [[NSWorkspace sharedWorkspace] openURL: 165 | [NSURL URLWithString:[NSString stringWithUTF8String:url]]]; 166 | } 167 | if (args == NULL) { 168 | args = ""; 169 | } 170 | 171 | char command_line[4096]; 172 | snprintf(command_line, sizeof(command_line), "'%s' %s '%s'", program, args, url); 173 | command_line[sizeof(command_line) - 1] = '\0'; 174 | 175 | wordexp_t expanded_args; 176 | memset(&expanded_args, 0, sizeof(expanded_args)); 177 | // On OS X, wordexp requires SIGCHLD. See: http://stackoverflow.com/questions/20534788/why-does-wordexp-fail-with-wrde-syntax-on-os-x 178 | signal(SIGCHLD, SIG_DFL); 179 | int result = wordexp(command_line, &expanded_args, 0); 180 | signal(SIGCHLD, SIG_IGN); 181 | if (result) { 182 | debug_log("Failed to parse command line: %s", command_line); 183 | return false; 184 | } 185 | browser_process_pid = fork(); 186 | if (!browser_process_pid) { 187 | // child process, launch the browser! 188 | execv(expanded_args.we_wordv[0], expanded_args.we_wordv); 189 | exit(1); 190 | } 191 | wordfree(&expanded_args); 192 | return true; 193 | } 194 | 195 | bool close_browser() { 196 | if (browser_process_pid == 0) { 197 | debug_log("Browser not open"); 198 | return false; 199 | } 200 | int r = kill(browser_process_pid, SIGKILL); 201 | browser_process_pid = 0; 202 | if (r) { 203 | debug_log("Failed to close browser window"); 204 | return false; 205 | } 206 | return true; 207 | } 208 | 209 | 210 | pid_t window_process_pid = 0; 211 | 212 | bool open_native_reference_window(uint8_t *test_pattern_for_window) { 213 | if (window_process_pid != 0) { 214 | debug_log("Native reference window already open"); 215 | return false; 216 | } 217 | char path[2048]; 218 | uint32_t length = sizeof(path); 219 | if (_NSGetExecutablePath(path, &length)) { 220 | debug_log("Couldn't find executable path"); 221 | return false; 222 | } 223 | char hex_pattern[hex_pattern_length + 1]; 224 | hex_encode_magic_pattern(test_pattern_for_window, hex_pattern); 225 | window_process_pid = fork(); 226 | if (!window_process_pid) { 227 | // Child process. It would be nice to just call into Cocoa from here, but 228 | // Cocoa can't handle running after a call to fork(), so instead we must 229 | // restart the process. 230 | execl(path, path, "-p", hex_pattern, NULL); 231 | } 232 | // Parent process. Wait for the child to launch and show its window before 233 | // returning. 234 | usleep(2000000 /* 2 seconds */); 235 | return true; 236 | } 237 | 238 | bool close_native_reference_window() { 239 | if (window_process_pid == 0) { 240 | debug_log("Native reference window not open"); 241 | return false; 242 | } 243 | int r = kill(window_process_pid, SIGKILL); 244 | window_process_pid = 0; 245 | if (r) { 246 | debug_log("Failed to close native reference window"); 247 | return false; 248 | } 249 | return true; 250 | } 251 | -------------------------------------------------------------------------------- /src/oculus.cpp: -------------------------------------------------------------------------------- 1 | #include "../third_party/LibOVR/Include/OVR.h" 2 | 3 | #ifndef _WINDOWS 4 | // On all platforms except Windows, the rest of the code is compiled as C. 5 | extern "C" { 6 | #endif 7 | #include "screenscraper.h" 8 | #include "oculus.h" 9 | #ifndef _WINDOWS 10 | } 11 | #endif 12 | 13 | static char result_buffer[2048]; 14 | static OVR::DeviceManager *manager = NULL; 15 | static OVR::LatencyTestDevice *global_latency_device = NULL; 16 | 17 | class Handler : public OVR::MessageHandler { 18 | virtual void OnMessage(const OVR::Message &message) { 19 | if (message.Type == OVR::Message_DeviceRemoved) { 20 | OVR::LatencyTestDevice *local_device = global_latency_device; 21 | global_latency_device = NULL; 22 | // Ugh, this isn't guaranteed to be thread safe but it's easier than 23 | // implementing a cross-platform lock primitive. Another alternative 24 | // would be to just never release the device. 25 | usleep(1000); 26 | local_device->Release(); 27 | } 28 | if (message.Type == OVR::Message_LatencyTestButton) { 29 | send_keystroke_t(); 30 | } 31 | } 32 | }; 33 | static Handler handler; 34 | 35 | // Installs an event handler on any created device so we can detect when the 36 | // device's button is pressed. 37 | static OVR::LatencyTestDevice *get_device() { 38 | OVR::LatencyTestDevice *local_device = global_latency_device; 39 | if(local_device == NULL) { 40 | local_device = 41 | manager->EnumerateDevices().CreateDevice(); 42 | if (local_device != NULL) { 43 | local_device->SetMessageHandler(&handler); 44 | global_latency_device = local_device; 45 | } 46 | } 47 | if (local_device) { 48 | local_device->AddRef(); 49 | } 50 | return local_device; 51 | } 52 | 53 | // Must be called before all other functions in this file. 54 | extern "C" void init_oculus() { 55 | OVR::System::Init(); 56 | manager = OVR::DeviceManager::Create(); 57 | OVR::LatencyTestDevice *latency_device = get_device(); 58 | } 59 | 60 | // Returns true if an Oculus Latency Tester is attached. 61 | extern "C" bool latency_tester_available() { 62 | assert(manager); 63 | OVR::LatencyTestDevice *latency_device = get_device(); 64 | if (latency_device) { 65 | latency_device->Release(); 66 | return true; 67 | } 68 | return false; 69 | } 70 | 71 | // Drives the Oculus Latency Tester to run a latency test. Works together 72 | // with hardware-latency-test.html, communicating using keystrokes ('B' means 73 | // draw black, 'W' means draw white. 74 | extern "C" bool run_hardware_latency_test(const char **result) { 75 | assert(manager); 76 | assert(result); 77 | *result = "Unknown error"; 78 | OVR::LatencyTestDevice *latency_device = get_device(); 79 | // Check that the latency tester is plugged in. 80 | if (!latency_device) { 81 | *result = "Oculus latency tester not found."; 82 | return false; 83 | } 84 | OVR::Util::LatencyTest latency_util; 85 | // LatencyTest needs to take over handling of messages for the device. 86 | latency_device->SetMessageHandler(NULL); 87 | latency_util.SetDevice(latency_device); 88 | latency_device->Release(); 89 | // Main test loop. 90 | latency_util.BeginTest(); 91 | int displayed_color = -1; 92 | while (true) { 93 | latency_util.ProcessInputs(); 94 | OVR::Color color; 95 | latency_util.DisplayScreenColor(color); 96 | if (color.R != displayed_color) { 97 | if (color.R == 255 && color.G == 255 && color.B == 255) { 98 | // Display white. 99 | send_keystroke_w(); 100 | } else if (color.R == 0 && color.G == 0 && color.B == 0) { 101 | // Display black. 102 | send_keystroke_b(); 103 | } else { 104 | // We can only display white or black. 105 | *result = "Unexpected color requested by latency tester."; 106 | latency_util.SetDevice(NULL); 107 | latency_device->SetMessageHandler(&handler); 108 | return false; 109 | } 110 | displayed_color = color.R; 111 | } 112 | const char *oculusResults = latency_util.GetResultsString(); 113 | if (oculusResults != NULL) { 114 | // Success! Copy the string into a buffer because it will be deallocated 115 | // when the LatencyTest instance goes away. 116 | strncpy(result_buffer, oculusResults, sizeof(result_buffer)); 117 | result_buffer[sizeof(result_buffer) - 1] = '\0'; 118 | *result = result_buffer; 119 | latency_util.SetDevice(NULL); 120 | latency_device->SetMessageHandler(&handler); 121 | return true; 122 | } 123 | usleep(1000); 124 | // TODO: timeout 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/oculus.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #ifndef WLB_OCULUS_H_ 18 | #define WLB_OCULUS_H_ 19 | 20 | #ifdef __cplusplus 21 | #define EXTERN_C extern "C" 22 | #else 23 | #define EXTERN_C 24 | #endif 25 | 26 | EXTERN_C void init_oculus(); 27 | EXTERN_C bool latency_tester_available(); 28 | EXTERN_C bool run_hardware_latency_test(const char **result_or_error); 29 | 30 | #endif // WLB_OCULUS_H_ -------------------------------------------------------------------------------- /src/screenscraper.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // This is a platform abstraction layer that encapsulates everything you need 18 | // to screenshot a browser window, send input events to it, and get precise 19 | // timing information. It is intended to be implementable on Mac, Windows, and 20 | // Linux (X11). Android and iOS would be cool too, eventually. 21 | 22 | #ifndef WLB_SCREENSCRAPER_H_ 23 | #define WLB_SCREENSCRAPER_H_ 24 | 25 | #include 26 | #include 27 | #if __STDC_VERSION__ >= 199901L // C99 28 | #include 29 | #endif 30 | #ifdef _WINDOWS 31 | // MSVC doesn't support C99, so we compile all the C code as C++ instead. 32 | #include 33 | #ifndef INFINITY 34 | #define INFINITY (std::numeric_limits::infinity()) 35 | #endif 36 | #include 37 | #define __sync_fetch_and_add _InterlockedExchangeAdd 38 | // Ugh, MSVC doesn't have a sensible snprintf. sprintf_s is close, as long as 39 | // you don't care about the return value. 40 | #define snprintf sprintf_s 41 | #endif 42 | 43 | typedef struct { 44 | uint32_t width, height; // The size of the image in pixels. 45 | uint32_t stride; // The distance between rows in memory, in bytes. 46 | const uint8_t *pixels; // 32-bit BGRA format, 4 * stride * height bytes. 47 | int64_t time_nanoseconds; // The moment when the screenshot was taken. 48 | void *platform_specific_data; 49 | } screenshot; 50 | 51 | // Takes a screenshot of the specified pixels. Width and height are 52 | // automatically clamped to the screen, so to take a full-screen screenshot 53 | // simply pass UINT32_MAX. May block until the next time something is drawn to 54 | // the screen, or return immediately, depending on platform. Large screenshots 55 | // may take a long time to acquire (100+ milliseconds), but small screenshots 56 | // should be fast (< 16 milliseconds). 57 | // May return NULL if taking a screenshot fails. 58 | screenshot *take_screenshot(uint32_t x, uint32_t y, uint32_t width, 59 | uint32_t height); 60 | void free_screenshot(screenshot *screenshot); 61 | 62 | // Sends key down and key up events to the foreground window for the named key. 63 | // Returns true on success, false on failure. 64 | bool send_keystroke_b(); 65 | bool send_keystroke_t(); 66 | bool send_keystroke_w(); 67 | bool send_keystroke_z(); 68 | 69 | // Warps the mouse to the given point and sends a mousewheel scroll down event. 70 | // Returns true on success, false on failure. 71 | bool send_scroll_down(int x, int y); 72 | 73 | // Returns the number of nanoseconds elapsed relative to some fixed point in the 74 | // past. The point to which this duration is relative does not change during the 75 | // lifetime of the process, but can change between different processes. 76 | int64_t get_nanoseconds(); 77 | static const int64_t nanoseconds_per_millisecond = 1000000; 78 | static const int64_t nanoseconds_per_second = 79 | nanoseconds_per_millisecond * 1000; 80 | 81 | // Sends a message to the debug console (which printf doesn't do on Windows...). 82 | // Accepts printf format strings. Always writes a newline at the end of the 83 | // message. 84 | void debug_log(const char *message, ...); 85 | 86 | // From unistd.h, but unistd.h is not available on Windows, so redefine it here. 87 | int usleep(unsigned int microseconds); 88 | 89 | // Opens a new window/tab in the system's default browser. 90 | // Returns true on success, false on failure. 91 | bool open_browser(const char *program, const char *args, const char *url); 92 | bool close_browser(); 93 | 94 | // Opens a test window that will respond to mouse and keyboard events in the 95 | // same way as a browser displaying the test page. Running the benchmark with 96 | // this test window will establish the best possible score achievable on a given 97 | // system. 98 | // Returns true on success, false on failure. 99 | bool open_native_reference_window(uint8_t *test_pattern); 100 | bool close_native_reference_window(); 101 | 102 | // The number of pixels in the pattern that encodes the data from the test window. 103 | static const int pattern_pixels = 8; 104 | static const int pattern_bytes = pattern_pixels * 4; 105 | // The "magic" part of the pattern uniquely identifies the test window on the screen. 106 | static const int pattern_magic_pixels = 4; 107 | static const int pattern_magic_bytes = pattern_magic_pixels * 4; 108 | // The data part of the pattern encodes the test progress. 109 | static const int data_pixels = pattern_pixels - pattern_magic_pixels; 110 | static const int data_bytes = data_pixels * 3; 111 | // The length of the magic part of the pattern, in characters, when it is 112 | // encoded as hexadecimal digits (omitting the alpha bytes). 113 | static const int hex_pattern_length = pattern_magic_pixels * 3 * 2; 114 | 115 | #endif // WLB_SCREENSCRAPER_H_ 116 | -------------------------------------------------------------------------------- /src/server.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include "screenscraper.h" 25 | #include "latency-benchmark.h" 26 | #include "../third_party/mongoose/mongoose.h" 27 | #include "oculus.h" 28 | #include "clioptions.h" 29 | 30 | //MSVC doesn't hvae snprintf defined, for our use, this works- beware they are not identical 31 | #ifdef WIN32 32 | #define snprintf sprintf_s 33 | #endif 34 | 35 | // Serve files from the ./html directory. 36 | char *document_root = "html"; 37 | struct mg_context *mongoose = NULL; 38 | 39 | // Runs a latency test and reports the results as JSON written to the given 40 | // connection. 41 | static void report_latency(struct mg_connection *connection, 42 | const uint8_t magic_pattern[]) { 43 | double key_down_latency_ms = 0; 44 | double scroll_latency_ms = 0; 45 | double max_js_pause_time_ms = 0; 46 | double max_css_pause_time_ms = 0; 47 | double max_scroll_pause_time_ms = 0; 48 | char *error = "Unknown error."; 49 | if (!measure_latency(magic_pattern, 50 | &key_down_latency_ms, 51 | &scroll_latency_ms, 52 | &max_js_pause_time_ms, 53 | &max_css_pause_time_ms, 54 | &max_scroll_pause_time_ms, 55 | &error)) { 56 | // Report generic error. 57 | debug_log("measure_latency reported error: %s", error); 58 | mg_printf(connection, "HTTP/1.1 500 Internal Server Error\r\n" 59 | "Access-Control-Allow-Origin: *\r\n" 60 | "Content-Type: text/plain\r\n\r\n" 61 | "%s", error); 62 | } else { 63 | // Send the measured latency information back as JSON. 64 | mg_printf(connection, "HTTP/1.1 200 OK\r\n" 65 | "Access-Control-Allow-Origin: *\r\n" 66 | "Cache-Control: no-cache\r\n" 67 | "Content-Type: text/plain\r\n\r\n" 68 | "{ \"keyDownLatencyMs\": %f, " 69 | "\"scrollLatencyMs\": %f, " 70 | "\"maxJSPauseTimeMs\": %f, " 71 | "\"maxCssPauseTimeMs\": %f, " 72 | "\"maxScrollPauseTimeMs\": %f}", 73 | key_down_latency_ms, 74 | scroll_latency_ms, 75 | max_js_pause_time_ms, 76 | max_css_pause_time_ms, 77 | max_scroll_pause_time_ms); 78 | } 79 | } 80 | 81 | // If the given request is a latency test request that specifies a valid 82 | // pattern, returns true and fills in the given array with the pattern specified 83 | // in the request's URL. 84 | static bool is_latency_test_request(const struct mg_request_info *request_info, 85 | uint8_t magic_pattern[]) { 86 | assert(magic_pattern); 87 | // A valid test request will have the path /test and must specify a magic 88 | // pattern in the magicPattern query variable. The pattern is specified as a 89 | // string of hex digits and must be the exact length expected (3 bytes for 90 | // each pixel in the pattern). 91 | // Here is an example of a valid request: 92 | // http://localhost:5578/test?magicPattern=8a36052d02c596dfa4c80711 93 | if (strcmp(request_info->uri, "/test") == 0) { 94 | char hex_pattern[hex_pattern_length + 1]; 95 | if (hex_pattern_length == mg_get_var( 96 | request_info->query_string, 97 | strlen(request_info->query_string), 98 | "magicPattern", 99 | hex_pattern, 100 | hex_pattern_length + 1)) { 101 | return parse_hex_magic_pattern(hex_pattern, magic_pattern); 102 | } 103 | } 104 | return false; 105 | } 106 | 107 | // This function is defined in the file generated by files-to-c-arrays.py 108 | const char *get_file(const char *path, size_t *out_size); 109 | 110 | // Satisfies the HTTP request from memory, or returns a 404 error. The 111 | // filesystem is never touched. 112 | // Ideally we'd use Mongoose's open_file callback override to implement file 113 | // serving from memory instead, but that method provides no way to disable 114 | // caching or display directory index documents. 115 | static void serve_file_from_memory_or_404(struct mg_connection *connection) { 116 | const struct mg_request_info *request_info = mg_get_request_info(connection); 117 | const char *uri = request_info->uri; 118 | // If the root of the server is requested, display the index instead. 119 | if (strlen(uri) < 2) { 120 | uri = "/index.html"; 121 | } 122 | // Construct the file's full path relative to the document root. 123 | const int max_path = 2048; 124 | char file_path[max_path]; 125 | size_t path_length = strlen(uri) + strlen(document_root) + 1; 126 | const char *file = NULL; 127 | size_t file_size = 0; 128 | if (path_length < max_path) { 129 | snprintf(file_path, path_length, "%s%s", document_root, uri); 130 | file = get_file(file_path, &file_size); 131 | } 132 | if (file) { 133 | // We've located the file in memory. Serve it with headers to disable 134 | // caching. 135 | mg_printf(connection, "HTTP/1.1 200 OK\r\n" 136 | "Cache-Control: no-cache\r\n" 137 | "Content-Type: %s\r\n" 138 | "Content-Length: %lu\r\n" 139 | "Connection: close\r\n\r\n", 140 | mg_get_builtin_mime_type(file_path), 141 | file_size); 142 | mg_write(connection, file, file_size); 143 | } else { 144 | // The file doesn't exist in memory. 145 | mg_printf(connection, "HTTP/1.1 404 Not Found\r\n" 146 | "Cache-Control: no-cache\r\n" 147 | "Content-Type: text/plain; charset=utf-8\r\n" 148 | "Content-Length: 25\r\n" 149 | "Connection: close\r\n\r\n" 150 | "Error 404: File not found"); 151 | } 152 | } 153 | 154 | 155 | // The number of pages holding open keep-alive requests to the server is stored 156 | // in this global counter, updated with atomic increment/decrement instructions. 157 | // When it reaches zero the server will exit. 158 | volatile long keep_alives = 0; 159 | 160 | static int mongoose_begin_request_callback(struct mg_connection *connection) { 161 | const struct mg_request_info *request_info = mg_get_request_info(connection); 162 | uint8_t magic_pattern[pattern_magic_bytes]; 163 | if (is_latency_test_request(request_info, magic_pattern)) { 164 | // This is an XMLHTTPRequest made by JavaScript to measure latency in a 165 | // browser window. magic_pattern has been filled in with a pixel pattern to 166 | // look for. 167 | report_latency(connection, magic_pattern); 168 | return 1; // Mark as processed 169 | } else if (strcmp(request_info->uri, "/keepServerAlive") == 0) { 170 | __sync_fetch_and_add(&keep_alives, 1); 171 | mg_printf(connection, "HTTP/1.1 200 OK\r\n" 172 | "Access-Control-Allow-Origin: *\r\n" 173 | "Content-Type: application/octet-stream\r\n" 174 | "Cache-Control: no-cache\r\n" 175 | "Transfer-Encoding: chunked\r\n\r\n"); 176 | const int chunk_size = 6; 177 | char *chunk0 = "1\r\n0\r\n"; 178 | char *chunk1 = "1\r\n1\r\n"; 179 | char *chunk = latency_tester_available() ? chunk1 : chunk0; 180 | const int warmup_chunks = 2048; 181 | for (int i = 0; i < warmup_chunks; i++) { 182 | mg_write(connection, chunk, chunk_size); 183 | } 184 | while(true) { 185 | chunk = latency_tester_available() ? chunk1 : chunk0; 186 | if (!mg_write(connection, chunk, chunk_size)) break; 187 | usleep(1000 * 1000); 188 | } 189 | __sync_fetch_and_add(&keep_alives, -1); 190 | return 1; 191 | } else if(strcmp(request_info->uri, "/runControlTest") == 0) { 192 | uint8_t *test_pattern = (uint8_t *)malloc(pattern_bytes); 193 | memset(test_pattern, 0, pattern_bytes); 194 | for (int i = 0; i < pattern_magic_bytes; i++) { 195 | test_pattern[i] = rand(); 196 | } 197 | open_native_reference_window(test_pattern); 198 | report_latency(connection, test_pattern); 199 | close_native_reference_window(); 200 | return 1; 201 | } else if (strcmp(request_info->uri, "/oculusLatencyTester") == 0) { 202 | const char *result_or_error = "Unknown error"; 203 | if (run_hardware_latency_test(&result_or_error)) { 204 | debug_log("hardware latency test succeeded"); 205 | mg_printf(connection, "HTTP/1.1 200 OK\r\n" 206 | "Access-Control-Allow-Origin: *\r\n" 207 | "Cache-Control: no-cache\r\n" 208 | "Content-Type: text/plain\r\n\r\n" 209 | "%s", result_or_error); 210 | } else { 211 | debug_log("hardware latency test failed"); 212 | mg_printf(connection, "HTTP/1.1 500 Internal Server Error\r\n" 213 | "Access-Control-Allow-Origin: *\r\n" 214 | "Cache-Control: no-cache\r\n" 215 | "Content-Type: text/plain\r\n\r\n" 216 | "%s", result_or_error); 217 | } 218 | return 1; 219 | } else { 220 | #ifdef NDEBUG 221 | // In release mode, we embed the test files in the executable and serve 222 | // them from memory. This makes the test easier to distribute as it is 223 | // a standalone executable file with no other dependencies. 224 | serve_file_from_memory_or_404(connection); 225 | return 1; 226 | #else 227 | // In debug mode, we serve the test files directly from the filesystem for 228 | // ease of development. Mongoose handles file serving for us. 229 | return 0; 230 | #endif 231 | } 232 | } 233 | 234 | // This is the entry point called by main(). 235 | void run_server(clioptions *opts) { 236 | assert(mongoose == NULL); 237 | srand((unsigned int)time(NULL)); 238 | init_oculus(); 239 | const char *options[] = { 240 | "listening_ports", "5578", 241 | "document_root", document_root, 242 | // Forbid everyone except localhost. 243 | "access_control_list", "-0.0.0.0/0,+127.0.0.0/8", 244 | // We have a lot of concurrent long-lived requests, so start a lot of 245 | // threads to make sure we can handle them all. 246 | "num_threads", "32", 247 | NULL 248 | }; 249 | struct mg_callbacks callbacks; 250 | memset(&callbacks, 0, sizeof(callbacks)); 251 | callbacks.begin_request = mongoose_begin_request_callback; 252 | 253 | mongoose = mg_start(&callbacks, NULL, options); 254 | if (!mongoose) { 255 | debug_log("Failed to start server."); 256 | exit(1); 257 | } 258 | usleep(0); 259 | 260 | char url[2048]; 261 | char *baseurl = "http://localhost:5578/"; 262 | char *results_url = opts->results_url; 263 | if (results_url == NULL) { 264 | results_url = ""; 265 | } 266 | if (opts->automated) { 267 | snprintf(url, sizeof(url), "%slatency-benchmark.html?auto=1&results=%s", baseurl, results_url); 268 | } else { 269 | snprintf(url, sizeof(url), "%s", baseurl); 270 | } 271 | url[sizeof(url) - 1] = '\0'; 272 | 273 | if (!open_browser(opts->browser, opts->browser_args, url)) { 274 | debug_log("Failed to open browser."); 275 | } 276 | // Wait for an initial keep-alive connection to be established. 277 | int64_t start_time = get_nanoseconds(); 278 | while(keep_alives == 0) { 279 | usleep(1000 * 1000); 280 | if (opts->automated && get_nanoseconds() - start_time > 5 * 60 *nanoseconds_per_second) { 281 | // 5 minute timeout in automated mode. 282 | break; 283 | } 284 | } 285 | // Wait for all keep-alive connections to be closed. 286 | while(keep_alives > 0) { 287 | // NOTE: If you are debugging using GDB or XCode, you may encounter signal 288 | // SIGPIPE on this line. SIGPIPE is harmless and you should configure your 289 | // debugger to ignore it. For instructions see here: 290 | // http://stackoverflow.com/questions/10431579/permanently-configuring-lldb-in-xcode-4-3-2-not-to-stop-on-signals 291 | // http://ricochen.wordpress.com/2011/07/14/debugging-with-gdb-a-couple-of-notes/ 292 | usleep(1000 * 100); 293 | if (opts->automated && get_nanoseconds() - start_time > 5 * 60 *nanoseconds_per_second) { 294 | // 5 minute timeout in automated mode. 295 | break; 296 | } 297 | } 298 | mg_stop(mongoose); 299 | 300 | if (opts->automated) { 301 | // NOTE: this only will work in automated mode where we fork and get the pid of the child process 302 | close_browser(); 303 | } 304 | 305 | mongoose = NULL; 306 | } 307 | -------------------------------------------------------------------------------- /src/win/getopt.c: -------------------------------------------------------------------------------- 1 | // http://docs.freeswitch.org/getopt_8c-source.html 2 | /***************************************************************************** 3 | * 4 | * MODULE NAME : GETOPT.C 5 | * 6 | * COPYRIGHTS: 7 | * This module contains code made available by IBM 8 | * Corporation on an AS IS basis. Any one receiving the 9 | * module is considered to be licensed under IBM copyrights 10 | * to use the IBM-provided source code in any way he or she 11 | * deems fit, including copying it, compiling it, modifying 12 | * it, and redistributing it, with or without 13 | * modifications. No license under any IBM patents or 14 | * patent applications is to be implied from this copyright 15 | * license. 16 | * 17 | * A user of the module should understand that IBM cannot 18 | * provide technical support for the module and will not be 19 | * responsible for any consequences of use of the program. 20 | * 21 | * Any notices, including this one, are not to be removed 22 | * from the module without the prior written consent of 23 | * IBM. 24 | * 25 | * AUTHOR: Original author: 26 | * G. R. Blair (BOBBLAIR at AUSVM1) 27 | * Internet: bobblair@bobblair.austin.ibm.com 28 | * 29 | * Extensively revised by: 30 | * John Q. Walker II, Ph.D. (JOHHQ at RALVM6) 31 | * Internet: johnq@ralvm6.vnet.ibm.com 32 | * 33 | *****************************************************************************/ 34 | 35 | /****************************************************************************** 36 | * getopt() 37 | * 38 | * The getopt() function is a command line parser. It returns the next 39 | * option character in argv that matches an option character in opstring. 40 | * 41 | * The argv argument points to an array of argc+1 elements containing argc 42 | * pointers to character strings followed by a null pointer. 43 | * 44 | * The opstring argument points to a string of option characters; if an 45 | * option character is followed by a colon, the option is expected to have 46 | * an argument that may or may not be separated from it by white space. 47 | * The external variable optarg is set to point to the start of the option 48 | * argument on return from getopt(). 49 | * 50 | * The getopt() function places in optind the argv index of the next argument 51 | * to be processed. The system initializes the external variable optind to 52 | * 1 before the first call to getopt(). 53 | * 54 | * When all options have been processed (that is, up to the first nonoption 55 | * argument), getopt() returns EOF. The special option "--" may be used to 56 | * delimit the end of the options; EOF will be returned, and "--" will be 57 | * skipped. 58 | * 59 | * The getopt() function returns a question mark (?) when it encounters an 60 | * option character not included in opstring. This error message can be 61 | * disabled by setting opterr to zero. Otherwise, it returns the option 62 | * character that was detected. 63 | * 64 | * If the special option "--" is detected, or all options have been 65 | * processed, EOF is returned. 66 | * 67 | * Options are marked by either a minus sign (-) or a slash (/). 68 | * 69 | * No errors are defined. 70 | *****************************************************************************/ 71 | 72 | #include /* for EOF */ 73 | #include /* for strchr() */ 74 | 75 | /* static (global) variables that are specified as exported by getopt() */ 76 | char *optarg = NULL; /* pointer to the start of the option argument */ 77 | int optind = 1; /* number of the next argv[] to be evaluated */ 78 | int opterr = 1; /* non-zero if a question mark should be returned 79 | when a non-valid option character is detected */ 80 | char optopt; /* value of the last character we matched */ 81 | 82 | /* handle possible future character set concerns by putting this in a macro */ 83 | #define _next_char(string) (char)(*(string+1)) 84 | 85 | int getopt(int argc, char **argv, char *opstring) 86 | { 87 | static char *pIndexPosition = NULL; /* place inside current argv string */ 88 | char *pArgString = NULL; /* where to start from next */ 89 | char *pOptString; /* the string in our program */ 90 | 91 | 92 | if (pIndexPosition != NULL) { 93 | /* we last left off inside an argv string */ 94 | if (*(++pIndexPosition)) { 95 | /* there is more to come in the most recent argv */ 96 | pArgString = pIndexPosition; 97 | } 98 | } 99 | 100 | if (pArgString == NULL) { 101 | /* we didn't leave off in the middle of an argv string */ 102 | if (optind >= argc) { 103 | /* more command-line arguments than the argument count */ 104 | pIndexPosition = NULL; /* not in the middle of anything */ 105 | return EOF; /* used up all command-line arguments */ 106 | } 107 | 108 | /*--------------------------------------------------------------------- 109 | * If the next argv[] is not an option, there can be no more options. 110 | *-------------------------------------------------------------------*/ 111 | pArgString = argv[optind++]; /* set this to the next argument ptr */ 112 | 113 | if (('/' != *pArgString) && /* doesn't start with a slash or a dash? */ 114 | ('-' != *pArgString)) { 115 | --optind; /* point to current arg once we're done */ 116 | optarg = NULL; /* no argument follows the option */ 117 | pIndexPosition = NULL; /* not in the middle of anything */ 118 | return EOF; /* used up all the command-line flags */ 119 | } 120 | 121 | /* check for special end-of-flags markers */ 122 | if ((strcmp(pArgString, "-") == 0) || 123 | (strcmp(pArgString, "--") == 0)) { 124 | optarg = NULL; /* no argument follows the option */ 125 | pIndexPosition = NULL; /* not in the middle of anything */ 126 | return EOF; /* encountered the special flag */ 127 | } 128 | 129 | pArgString++; /* look past the / or - */ 130 | } 131 | 132 | if (':' == *pArgString) { /* is it a colon? */ 133 | /*--------------------------------------------------------------------- 134 | * Rare case: if opterr is non-zero, return a question mark; 135 | * otherwise, just return the colon we're on. 136 | *-------------------------------------------------------------------*/ 137 | optopt = *pArgString; 138 | return (opterr ? (int)'?' : (int)':'); 139 | } 140 | else if ((pOptString = strchr(opstring, *pArgString)) == 0) { 141 | /*--------------------------------------------------------------------- 142 | * The letter on the command-line wasn't any good. 143 | *-------------------------------------------------------------------*/ 144 | optarg = NULL; /* no argument follows the option */ 145 | pIndexPosition = NULL; /* not in the middle of anything */ 146 | optopt = *pArgString; 147 | return (opterr ? (int)'?' : (int)*pArgString); 148 | } 149 | else { 150 | /*--------------------------------------------------------------------- 151 | * The letter on the command-line matches one we expect to see 152 | *-------------------------------------------------------------------*/ 153 | if (':' == _next_char(pOptString)) { /* is the next letter a colon? */ 154 | /* It is a colon. Look for an argument string. */ 155 | if ('\0' != _next_char(pArgString)) { /* argument in this argv? */ 156 | optarg = &pArgString[1]; /* Yes, it is */ 157 | } 158 | else { 159 | /*------------------------------------------------------------- 160 | * The argument string must be in the next argv. 161 | * But, what if there is none (bad input from the user)? 162 | * In that case, return the letter, and optarg as NULL. 163 | *-----------------------------------------------------------*/ 164 | if (optind < argc) 165 | optarg = argv[optind++]; 166 | else { 167 | optarg = NULL; 168 | optopt = *pArgString; 169 | return (opterr ? (int)'?' : (int)*pArgString); 170 | } 171 | } 172 | pIndexPosition = NULL; /* not in the middle of anything */ 173 | } 174 | else { 175 | /* it's not a colon, so just return the letter */ 176 | optarg = NULL; /* no argument follows the option */ 177 | pIndexPosition = pArgString; /* point to the letter we're on */ 178 | } 179 | return (int)*pArgString; /* return the letter that matched */ 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/win/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "stdafx.h" 18 | #include "../latency-benchmark.h" 19 | #include "../screenscraper.h" 20 | #include "../clioptions.h" 21 | 22 | void run_server(clioptions*); 23 | 24 | static BOOL (APIENTRY *wglSwapIntervalEXT)(int) = 0; 25 | static HGLRC context = NULL; 26 | static uint8_t pattern[pattern_bytes]; 27 | static int scrolls = 0; 28 | static int key_downs = 0; 29 | static int esc_presses = 0; 30 | 31 | LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) 32 | { 33 | int64_t paint_time = get_nanoseconds(); 34 | switch(msg) { 35 | case WM_DESTROY: 36 | PostQuitMessage(0); 37 | break; 38 | case WM_MOUSEWHEEL: 39 | scrolls++; 40 | InvalidateRect(hwnd, NULL, false); 41 | break; 42 | case WM_KEYDOWN: 43 | if (wParam == VK_ESCAPE) { 44 | esc_presses++; 45 | } 46 | key_downs++; 47 | InvalidateRect(hwnd, NULL, false); 48 | break; 49 | case WM_PAINT: 50 | PAINTSTRUCT ps; 51 | BeginPaint(hwnd, &ps); 52 | wglMakeCurrent(ps.hdc, context); 53 | draw_pattern_with_opengl(pattern, scrolls, key_downs, esc_presses); 54 | SwapBuffers(ps.hdc); 55 | EndPaint(hwnd, &ps); 56 | break; 57 | default: 58 | return DefWindowProc(hwnd, msg, wParam, lParam); 59 | } 60 | return 0; 61 | } 62 | 63 | 64 | static const char *window_class_name = "window_class_name"; 65 | void message_loop(HANDLE parent_process) { 66 | WNDCLASS window_class; 67 | if (!GetClassInfo(GetModuleHandle(NULL), window_class_name, &window_class)) { 68 | memset(&window_class, 0, sizeof(WNDCLASS)); 69 | window_class.hInstance = GetModuleHandle(NULL); 70 | window_class.lpszClassName = window_class_name; 71 | window_class.hbrBackground = (HBRUSH)(COLOR_WINDOWTEXT + 1); 72 | window_class.lpfnWndProc = WndProc; 73 | ATOM a = RegisterClass(&window_class); 74 | assert(a); 75 | } 76 | HWND native_reference_window = NULL; 77 | native_reference_window = CreateWindowEx(WS_EX_TOPMOST, // Top most window and No taskbar button 78 | window_class_name, 79 | "Web Latency Benchmark test window", 80 | WS_DISABLED | WS_POPUP, // Borderless and user-inputs Disabled 81 | 200, 200, pattern_pixels, 1, 82 | NULL, NULL, window_class.hInstance, NULL); 83 | assert(native_reference_window); 84 | PIXELFORMATDESCRIPTOR pfd; 85 | ZeroMemory(&pfd, sizeof(pfd)); 86 | pfd.nSize = sizeof(pfd); 87 | pfd.nVersion = 1; 88 | pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER | PFD_DEPTH_DONTCARE; 89 | pfd.iPixelType = PFD_TYPE_RGBA; 90 | pfd.cColorBits = 32; 91 | HDC hdc = GetDC(native_reference_window); 92 | int pixel_format = ChoosePixelFormat(hdc, &pfd); 93 | assert(pixel_format); 94 | if (!SetPixelFormat(hdc, pixel_format, &pfd)) { 95 | debug_log("SetPixelFormat failed"); 96 | exit(1); 97 | } 98 | context = wglCreateContext(hdc); 99 | assert(context); 100 | if (!wglMakeCurrent(hdc, context)) { 101 | debug_log("Failed to init OpenGL"); 102 | exit(1); 103 | } 104 | wglSwapIntervalEXT = (BOOL (APIENTRY *)(int)) wglGetProcAddress("wglSwapIntervalEXT"); 105 | if (wglSwapIntervalEXT == NULL) { 106 | debug_log("Failed to get wglSwapIntervalEXT"); 107 | exit(1); 108 | } 109 | if (!wglSwapIntervalEXT(0)) { 110 | debug_log("Failed to disable vsync"); 111 | exit(1); 112 | } 113 | ReleaseDC(native_reference_window, hdc); 114 | ShowWindow(native_reference_window, SW_SHOWNORMAL); 115 | SetWindowPos(native_reference_window, HWND_TOPMOST, 0, 0, 0, 0, 116 | SWP_NOMOVE | SWP_NOSIZE); 117 | SetForegroundWindow(native_reference_window); 118 | UpdateWindow(native_reference_window); 119 | 120 | MSG msg; 121 | while(1) { 122 | while(!PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { 123 | InvalidateRect(native_reference_window, NULL, false); 124 | if (parent_process && WaitForSingleObject(parent_process, 0) != WAIT_TIMEOUT) { 125 | debug_log("parent process died, exiting"); 126 | exit(1); 127 | } 128 | usleep(1000); 129 | } 130 | if (msg.message == WM_QUIT) 131 | break; 132 | TranslateMessage(&msg); 133 | DispatchMessage(&msg); 134 | } 135 | } 136 | 137 | // Override the default behavior for assertion failures to break into the 138 | // debugger. 139 | int __cdecl CrtDbgHook(int nReportType, char* szMsg, int* pnRet) { 140 | // Break into the debugger, and then report that the exception was handled. 141 | _CrtDbgBreak(); 142 | return TRUE; 143 | } 144 | 145 | int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, 146 | _In_opt_ HINSTANCE hPrevInstance, 147 | _In_ LPTSTR lpCmdLine, 148 | _In_ int nCmdShow) 149 | { 150 | UNREFERENCED_PARAMETER(hInstance); 151 | UNREFERENCED_PARAMETER(hPrevInstance); 152 | UNREFERENCED_PARAMETER(lpCmdLine); 153 | UNREFERENCED_PARAMETER(nCmdShow); 154 | debug_log("starting process"); 155 | 156 | // Prevent error dialogs. 157 | _set_error_mode(_OUT_TO_STDERR); 158 | _CrtSetReportHook(CrtDbgHook); 159 | SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | 160 | SEM_NOOPENFILEERRORBOX); 161 | // Make stderr/stdout work from a non-console app. 162 | if (AttachConsole(ATTACH_PARENT_PROCESS)) { 163 | freopen("CONOUT$", "wb", stdout); 164 | freopen("CONOUT$", "wb", stderr); 165 | } 166 | 167 | // The Visual Studio debugger starts us in the build\ subdirectory by 168 | // default, which will prevent us from finding the test files in the html\ 169 | // directory. This workaround will locate the html\ directory even if it's 170 | // in the parent directory so we will work in the debugger out of the box. 171 | WIN32_FIND_DATA find_data; 172 | HANDLE h = FindFirstFile("html", &find_data); 173 | if (h == INVALID_HANDLE_VALUE) { 174 | h = FindFirstFile("..\\html", &find_data); 175 | if (h != INVALID_HANDLE_VALUE) { 176 | SetCurrentDirectory(".."); 177 | } 178 | } 179 | if (h != INVALID_HANDLE_VALUE) { 180 | FindClose(h); 181 | } 182 | 183 | // Prevent automatic DPI scaling. 184 | SetProcessDPIAware(); 185 | 186 | clioptions opts; 187 | parse_commandline(__argc, (const char **)__argv, &opts); 188 | 189 | if (opts.magic_pattern) { 190 | assert(opts.parent_handle); 191 | debug_log("opening native reference window"); 192 | // The -p argument is the magic pattern to draw on the window, encoded as hex. 193 | memset(pattern, 0, sizeof(pattern)); 194 | if (!parse_hex_magic_pattern(opts.magic_pattern, pattern)) { 195 | debug_log("Failed to parse pattern"); 196 | return 1; 197 | } 198 | // The -h argument is the HANDLE of the parent process, encoded as hex. 199 | HANDLE parent_process = (HANDLE)_strtoui64(opts.parent_handle, NULL, 16); 200 | message_loop(parent_process); 201 | return 0; 202 | } 203 | 204 | debug_log("running server"); 205 | HDC desktop = GetDC(NULL); 206 | assert(desktop); 207 | if (GetDeviceCaps(desktop, LOGPIXELSX) != 96) { 208 | MessageBox(NULL, "Unfortunately, due to browser bugs you must set the Windows DPI scaling factor to \"100%\" or \"Smaller\" (96 DPI) for this test to work. Please change it and reboot.", 209 | "Unsupported DPI", MB_ICONERROR | MB_OK); 210 | WinExec("DpiScaling.exe", SW_NORMAL); 211 | return 1; 212 | } 213 | ReleaseDC(NULL, desktop); 214 | run_server(&opts); 215 | return 0; 216 | } 217 | -------------------------------------------------------------------------------- /src/win/screenscraper.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "stdafx.h" 18 | #include "../latency-benchmark.h" 19 | 20 | static INIT_ONCE start_time_init_once; 21 | static LARGE_INTEGER start_time; 22 | static LARGE_INTEGER frequency; 23 | BOOL CALLBACK init_start_time(PINIT_ONCE ignored, void *ignored2, 24 | void **ignored3) { 25 | BOOL r; 26 | r = QueryPerformanceCounter(&start_time); 27 | assert(r); 28 | r = QueryPerformanceFrequency(&frequency); 29 | assert(r); 30 | return TRUE; 31 | } 32 | 33 | 34 | int64_t get_nanoseconds_elapsed(const LARGE_INTEGER ¤t_time) { 35 | BOOL r = InitOnceExecuteOnce(&start_time_init_once, &init_start_time, NULL, 36 | NULL); 37 | assert(r); 38 | int64_t elapsed = current_time.QuadPart - start_time.QuadPart; 39 | if (elapsed < 0) { 40 | elapsed = 0; 41 | } 42 | // We switch to floating point here before the division to avoid overflow 43 | // and/or loss of precision. We can't do this in 64-bit signed integer math: 44 | // doing the multiplication first would overflow in a matter of seconds, while 45 | // doing the division first risks loss of accuracy if the timer resolution is 46 | // close to the nanosecond range. 47 | double elapsed_ns = 48 | elapsed * (nanoseconds_per_second / (double)frequency.QuadPart); 49 | return (int64_t) elapsed_ns; 50 | } 51 | 52 | 53 | // Returns the number of nanoseconds elapsed since the first call to 54 | // get_nanoseconds in this process. The first call always returns 0. 55 | int64_t get_nanoseconds() { 56 | LARGE_INTEGER current_time; 57 | BOOL r = QueryPerformanceCounter(¤t_time); 58 | assert(r); 59 | return get_nanoseconds_elapsed(current_time); 60 | } 61 | 62 | 63 | static INIT_ONCE directx_initialization; 64 | static CRITICAL_SECTION directx_critical_section; 65 | static ID3D11Device *device = NULL; 66 | static ID3D11DeviceContext *context = NULL; 67 | static IDXGIFactory2 *dxgi_factory = NULL; 68 | static IDXGIAdapter1 *dxgi_adapter = NULL; 69 | static IDXGIOutput *dxgi_output = NULL; 70 | static IDXGIOutput1 *dxgi_output1 = NULL; 71 | static IDXGIOutputDuplication *dxgi_output_duplication = NULL; 72 | 73 | BOOL CALLBACK create_device(PINIT_ONCE ignored, void *ignored2, 74 | void **ignored3) { 75 | debug_log("creating device"); 76 | HRESULT hr; 77 | hr = CreateDXGIFactory1(__uuidof(IDXGIFactory2), (void **)&dxgi_factory); 78 | assert(hr == S_OK); 79 | hr = dxgi_factory->EnumAdapters1(0, &dxgi_adapter); 80 | assert(hr == S_OK); 81 | hr = dxgi_adapter->EnumOutputs(0, &dxgi_output); 82 | assert(hr == S_OK); 83 | hr = dxgi_output->QueryInterface(__uuidof(IDXGIOutput1), 84 | (void **)&dxgi_output1); 85 | assert(hr == S_OK); 86 | const D3D_FEATURE_LEVEL levels[] = { D3D_FEATURE_LEVEL_11_0 }; 87 | D3D_FEATURE_LEVEL out_level; 88 | UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; 89 | #ifndef NDEBUG 90 | flags |= D3D11_CREATE_DEVICE_DEBUG; 91 | #endif 92 | hr = D3D11CreateDevice(dxgi_adapter, D3D_DRIVER_TYPE_UNKNOWN, NULL, flags, 93 | levels, 1, D3D11_SDK_VERSION, &device, &out_level, &context); 94 | assert(hr == S_OK); 95 | hr = dxgi_output1->DuplicateOutput(device, &dxgi_output_duplication); 96 | assert(hr == S_OK); 97 | InitializeCriticalSection(&directx_critical_section); 98 | return TRUE; 99 | } 100 | 101 | 102 | static int64_t last_screenshot_time = 0; 103 | static screenshot *take_screenshot_with_dxgi(uint32_t x, uint32_t y, 104 | uint32_t width, uint32_t height) { 105 | HRESULT hr = S_OK; 106 | BOOL r = 107 | InitOnceExecuteOnce(&directx_initialization, &create_device, NULL, NULL); 108 | assert(r); 109 | EnterCriticalSection(&directx_critical_section); 110 | if (dxgi_output_duplication == NULL) { 111 | hr = dxgi_output1->DuplicateOutput(device, &dxgi_output_duplication); 112 | if (hr != S_OK) { 113 | debug_log("Failed to create output duplication interface."); 114 | return false; 115 | } 116 | } 117 | DXGI_OUTDUPL_FRAME_INFO frame_info; 118 | frame_info.AccumulatedFrames = 0; 119 | IDXGIResource *screen_resource = NULL; 120 | while(true) { 121 | hr = dxgi_output_duplication->AcquireNextFrame(INFINITE, &frame_info, 122 | &screen_resource); 123 | if (hr == S_OK && frame_info.AccumulatedFrames > 0) { 124 | // A new screenshot was taken. 125 | if (frame_info.AccumulatedFrames > 1) { 126 | debug_log("more than one frame at a time: %d frames", 127 | frame_info.AccumulatedFrames); 128 | } 129 | int64_t new_screenshot_time = 130 | get_nanoseconds_elapsed(frame_info.LastPresentTime); 131 | last_screenshot_time = new_screenshot_time; 132 | break; 133 | } else if (hr != S_OK) { 134 | // This happens if the screensaver came on or the computer went to sleep. 135 | // Try recreating the output duplication interface before giving up. 136 | debug_log("AcquireNextFrame failed"); 137 | dxgi_output_duplication->Release(); 138 | dxgi_output_duplication = NULL; 139 | hr = dxgi_output1->DuplicateOutput(device, &dxgi_output_duplication); 140 | if (hr != S_OK) { 141 | debug_log("Failed to recreate output duplication interface."); 142 | return false; 143 | } 144 | } else { 145 | // This was a mouse movement, not a screenshot. Try again. 146 | screen_resource->Release(); 147 | dxgi_output_duplication->ReleaseFrame(); 148 | } 149 | } 150 | ID3D11Texture2D *framebuffer = NULL; 151 | hr = screen_resource->QueryInterface(__uuidof(ID3D11Texture2D), 152 | (void **)&framebuffer); 153 | assert(hr == S_OK); 154 | D3D11_TEXTURE2D_DESC framebuffer_desc; 155 | framebuffer->GetDesc(&framebuffer_desc); 156 | assert(framebuffer_desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM); 157 | // Clamp width and height. 158 | width = min(width, framebuffer_desc.Width - x); 159 | height = min(height, framebuffer_desc.Height - y); 160 | if (x >= framebuffer_desc.Width || y >= framebuffer_desc.Height || 161 | width == 0 || height == 0) { 162 | debug_log("Invalid rectangle for screenshot."); 163 | framebuffer->Release(); 164 | screen_resource->Release(); 165 | LeaveCriticalSection(&directx_critical_section); 166 | return NULL; 167 | } 168 | ID3D11Texture2D *screenshot_texture = NULL; 169 | D3D11_TEXTURE2D_DESC screenshot_desc; 170 | D3D11_MAPPED_SUBRESOURCE screenshot_mapped; 171 | screenshot_desc = framebuffer_desc; 172 | screenshot_desc.Width = width; 173 | screenshot_desc.Height = height; 174 | screenshot_desc.Usage = D3D11_USAGE_STAGING; 175 | screenshot_desc.CPUAccessFlags = 176 | D3D11_CPU_ACCESS_READ | D3D11_CPU_ACCESS_WRITE; 177 | screenshot_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; 178 | screenshot_desc.MipLevels = screenshot_desc.ArraySize = 1; 179 | screenshot_desc.SampleDesc.Count = 1; 180 | screenshot_desc.BindFlags = screenshot_desc.MiscFlags = 0; 181 | device->CreateTexture2D(&screenshot_desc, NULL, &screenshot_texture); 182 | D3D11_BOX box; 183 | box.left = x; 184 | box.top = y; 185 | box.right = x + width; 186 | box.bottom = y + height; 187 | box.front = 0; 188 | box.back = 1; 189 | context->CopySubresourceRegion(screenshot_texture, 0, 0, 0, 0, framebuffer, 0, 190 | &box); 191 | hr = context->Map(screenshot_texture, 0, D3D11_MAP_READ_WRITE, 0, 192 | &screenshot_mapped); 193 | assert(hr == S_OK); 194 | screenshot *screen = (screenshot *)malloc(sizeof(screenshot)); 195 | screen->width = width; 196 | screen->height = height; 197 | screen->stride = screenshot_mapped.RowPitch; 198 | screen->pixels = (uint8_t *)screenshot_mapped.pData; 199 | screen->time_nanoseconds = last_screenshot_time; 200 | screen->platform_specific_data = screenshot_texture; 201 | framebuffer->Release(); 202 | screen_resource->Release(); 203 | hr = dxgi_output_duplication->ReleaseFrame(); 204 | assert(hr == S_OK); 205 | LeaveCriticalSection(&directx_critical_section); 206 | return screen; 207 | } 208 | 209 | 210 | bool composition_disabled = false; 211 | static screenshot *take_screenshot_with_gdi(uint32_t x, uint32_t y, 212 | uint32_t width, uint32_t height) { 213 | HRESULT hr; 214 | int ir; 215 | BOOL r; 216 | // DWM causes screenshotting with GDI to be very slow. The only fix is to 217 | // disable DWM during the test. DWM will automatically be re-enabled when the 218 | // process exits. This API stops working on Windows 8 (though it returns 219 | // success regardless). 220 | if (!composition_disabled) { 221 | // HACK: DwmEnableComposition fails unless GetDC(NULL) has been called 222 | // first. Who knows why. 223 | HDC workaround = GetDC(NULL); 224 | int ir = ReleaseDC(NULL, workaround); 225 | assert(ir); 226 | hr = DwmEnableComposition(DWM_EC_DISABLECOMPOSITION); 227 | assert(hr == S_OK); 228 | // DWM takes a little while to repain the screen after being disabled. 229 | Sleep(1000); 230 | composition_disabled = true; 231 | } 232 | int virtual_x = ((int)x) - GetSystemMetrics(SM_XVIRTUALSCREEN); 233 | int virtual_y = ((int)y) - GetSystemMetrics(SM_YVIRTUALSCREEN); 234 | width = min(width, (uint32_t)GetSystemMetrics(SM_CXVIRTUALSCREEN)); 235 | height = min(height, (uint32_t)GetSystemMetrics(SM_CYVIRTUALSCREEN)); 236 | HDC screen_dc = GetDC(NULL); 237 | assert(screen_dc); 238 | HDC memory_dc = CreateCompatibleDC(screen_dc); 239 | assert(memory_dc); 240 | BITMAPINFO bitmap_info; 241 | memset(&bitmap_info, 0, sizeof(BITMAPINFO)); 242 | bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); 243 | bitmap_info.bmiHeader.biWidth = width; 244 | bitmap_info.bmiHeader.biHeight = -(int)height; 245 | bitmap_info.bmiHeader.biPlanes = 1; 246 | bitmap_info.bmiHeader.biBitCount = 32; 247 | bitmap_info.bmiHeader.biCompression = BI_RGB; 248 | uint8_t *pixels = NULL; 249 | HBITMAP hbitmap = CreateDIBSection(screen_dc, &bitmap_info, DIB_RGB_COLORS, 250 | (void **)&pixels, NULL, 0); 251 | assert(hbitmap); 252 | SelectObject(memory_dc, hbitmap); 253 | r = BitBlt(memory_dc, 0, 0, width, height, screen_dc, virtual_x, virtual_y, 254 | SRCCOPY); 255 | assert(r); 256 | ir = ReleaseDC(NULL, screen_dc); 257 | assert(ir); 258 | r = DeleteObject(memory_dc); 259 | assert(r); 260 | screenshot *shot = (screenshot *)malloc(sizeof(screenshot)); 261 | shot->pixels = pixels; 262 | shot->width = width; 263 | shot->height = height; 264 | shot->stride = width * 4; 265 | shot->platform_specific_data = hbitmap; 266 | shot->time_nanoseconds = get_nanoseconds(); 267 | return shot; 268 | } 269 | 270 | 271 | static bool use_dxgi() { 272 | OSVERSIONINFO version; 273 | memset(&version, 0, sizeof(OSVERSIONINFO)); 274 | version.dwOSVersionInfoSize = sizeof(OSVERSIONINFO); 275 | BOOL r = GetVersionEx(&version); 276 | assert(r); 277 | bool windows_8_or_greater = version.dwMajorVersion > 6 || 278 | (version.dwMajorVersion == 6 && version.dwMinorVersion >= 2); 279 | return windows_8_or_greater; 280 | } 281 | 282 | 283 | screenshot *take_screenshot(uint32_t x, uint32_t y, uint32_t width, 284 | uint32_t height) { 285 | if (use_dxgi()) { 286 | // On Windows 8+ we use the DXGI 1.2 Desktop Duplication API. 287 | return take_screenshot_with_dxgi(x, y, width, height); 288 | } else { 289 | // Before Windows 8 we use GDI to take the screenshot. 290 | return take_screenshot_with_gdi(x, y, width, height); 291 | } 292 | } 293 | 294 | 295 | void free_screenshot(screenshot *shot) { 296 | if (use_dxgi()) { 297 | EnterCriticalSection(&directx_critical_section); 298 | ID3D11Texture2D *texture = (ID3D11Texture2D *)shot->platform_specific_data; 299 | context->Unmap(texture, 0); 300 | texture->Release(); 301 | free(shot); 302 | LeaveCriticalSection(&directx_critical_section); 303 | } else { 304 | DeleteObject((HBITMAP)shot->platform_specific_data); 305 | free(shot); 306 | } 307 | } 308 | 309 | 310 | // Sends a keydown+keyup event for the given key to the foreground window. 311 | static bool send_keystroke(WORD key_code) { 312 | INPUT input[2]; 313 | memset(input, 0, sizeof(INPUT) * 2); 314 | input[0].type = INPUT_KEYBOARD; 315 | input[0].ki.wVk = key_code; 316 | input[1].type = INPUT_KEYBOARD; 317 | input[1].ki.wVk = key_code; 318 | input[1].ki.dwFlags = KEYEVENTF_KEYUP; 319 | SendInput(2, input, sizeof(INPUT)); 320 | return true; 321 | } 322 | 323 | bool send_keystroke_b() { return send_keystroke(0x42); } 324 | bool send_keystroke_t() { return send_keystroke(0x54); } 325 | bool send_keystroke_w() { return send_keystroke(0x57); } 326 | bool send_keystroke_z() { return send_keystroke(0x5A); } 327 | 328 | bool send_scroll_down(int x, int y) { 329 | SetCursorPos(x, y); 330 | INPUT input; 331 | memset(&input, 0, sizeof(INPUT)); 332 | input.type = INPUT_MOUSE; 333 | input.mi.dwFlags = MOUSEEVENTF_WHEEL; 334 | input.mi.mouseData = -WHEEL_DELTA; 335 | SendInput(1, &input, sizeof(INPUT)); 336 | return true; 337 | } 338 | 339 | 340 | static const int log_buffer_size = 1000; 341 | void debug_log(const char *message, ...) { 342 | #ifndef NDEBUG 343 | char buf[log_buffer_size]; 344 | va_list args; 345 | va_start(args, message); 346 | vsnprintf_s(buf, _TRUNCATE, message, args); 347 | va_end(args); 348 | OutputDebugStringA(buf); 349 | OutputDebugStringA("\n"); 350 | printf("%s\n", buf); 351 | #endif 352 | } 353 | 354 | void always_log(const char *message, ...) { 355 | char buf[log_buffer_size]; 356 | va_list args; 357 | va_start(args, message); 358 | vsnprintf_s(buf, _TRUNCATE, message, args); 359 | va_end(args); 360 | OutputDebugStringA(buf); 361 | OutputDebugStringA("\n"); 362 | printf("%s\n", buf); 363 | } 364 | 365 | 366 | int usleep(unsigned int microseconds) { 367 | Sleep(microseconds / 1000); 368 | return 0; 369 | } 370 | 371 | HANDLE browser_process_handle = NULL; 372 | 373 | bool open_browser(const char *program, const char *args, const char *url) { 374 | // Recommended by: 375 | // http://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx 376 | 377 | if (program == NULL || strcmp(program, "") == 0) { 378 | HRESULT hr = 379 | CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE); 380 | assert(hr == S_OK); 381 | return 32 < (int)ShellExecuteA(NULL, "open", url, args, NULL, SW_SHOWNORMAL); 382 | } 383 | 384 | if (args == NULL) { 385 | args = ""; 386 | } 387 | char command_line[4096]; 388 | sprintf_s(command_line, sizeof(command_line), "\"%s\" %s \"%s\"", program, args, url); 389 | 390 | PROCESS_INFORMATION process_info; 391 | STARTUPINFO startup_info; 392 | memset(&startup_info, 0, sizeof(startup_info)); 393 | startup_info.cb = sizeof(startup_info); 394 | if (!CreateProcess(NULL, command_line, NULL, NULL, TRUE, 0, NULL, NULL, 395 | &startup_info, &process_info)) { 396 | debug_log("Failed to start process"); 397 | return false; 398 | } 399 | browser_process_handle = process_info.hProcess; 400 | return true; 401 | } 402 | 403 | bool close_browser() { 404 | if (browser_process_handle == NULL) { 405 | debug_log("browser not open"); 406 | return false; 407 | } 408 | if (!TerminateProcess(browser_process_handle, 0)) { 409 | debug_log("Failed to terminate process"); 410 | browser_process_handle = NULL; 411 | return false; 412 | } 413 | browser_process_handle = NULL; 414 | return true; 415 | } 416 | 417 | HANDLE window_process_handle = NULL; 418 | 419 | bool open_native_reference_window(uint8_t *test_pattern_for_window) { 420 | // The native reference window is opened in a new child process to make the 421 | // test more fair. Unfortunately Visual Studio can't automatically attach to 422 | // child processes. WinDbg can, so you can use WinDbg to debug the native 423 | // reference window process. If that's too painful, you can configure 424 | // Visual Studio to pass arguments on startup to debug the native reference 425 | // window code in isolation. Here are some sample arguments that will work: 426 | // -p 2923BEE16CD6529049F1BBE9 -h 0 427 | 428 | if (window_process_handle != NULL) { 429 | debug_log("native window already open"); 430 | return false; 431 | } 432 | PROCESS_INFORMATION process_info; 433 | char hex_pattern[hex_pattern_length + 1]; 434 | hex_encode_magic_pattern(test_pattern_for_window, hex_pattern); 435 | STARTUPINFO startup_info; 436 | memset(&startup_info, 0, sizeof(startup_info)); 437 | startup_info.cb = sizeof(startup_info); 438 | char command_line[4096]; 439 | HANDLE current_process_handle = NULL; 440 | if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), 441 | GetCurrentProcess(), ¤t_process_handle, 0, TRUE, 442 | DUPLICATE_SAME_ACCESS)) { 443 | debug_log("DuplicateHandle failed"); 444 | return false; 445 | } 446 | int numArgs; 447 | HMODULE exe = GetModuleHandle(NULL); 448 | char filename_of_exe[2048]; 449 | DWORD result = GetModuleFileName(exe, filename_of_exe, sizeof(filename_of_exe)); 450 | assert(result); 451 | sprintf_s(command_line, sizeof(command_line), "\"%s\" -p %s -h %X", filename_of_exe, 452 | hex_pattern, current_process_handle); 453 | if (!CreateProcess(NULL, command_line, NULL, NULL, TRUE, 0, NULL, NULL, 454 | &startup_info, &process_info)) { 455 | debug_log("Failed to start process"); 456 | return false; 457 | } 458 | window_process_handle = process_info.hProcess; 459 | // Wait for window show animation to finish. 460 | usleep(1000 * 1000 * 2); 461 | return true; 462 | } 463 | 464 | bool close_native_reference_window() { 465 | if (window_process_handle == NULL) { 466 | debug_log("native window not open"); 467 | return false; 468 | } 469 | if (!TerminateProcess(window_process_handle, 0)) { 470 | debug_log("Failed to terminate process"); 471 | window_process_handle = NULL; 472 | return false; 473 | } 474 | window_process_handle = NULL; 475 | return true; 476 | } 477 | -------------------------------------------------------------------------------- /src/win/stdafx.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // stdafx.h : include file for standard system include files, 18 | // or project specific include files that are used frequently, but 19 | // are changed infrequently 20 | // 21 | 22 | #ifndef WLB_WIN_STDAFX_H_ 23 | #define WLB_WIN_STDAFX_H_ 24 | 25 | // Including SDKDDKVer.h defines the highest available Windows platform. 26 | 27 | // If you wish to build your application for a previous Windows platform, include WinSDKVer.h and 28 | // set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. 29 | 30 | #include 31 | 32 | #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers 33 | // Windows Header Files: 34 | #include 35 | 36 | // C RunTime Header Files 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | 43 | // The above is automatically generated by Visual Studio as part of its default Windows project 44 | // template. Below are additional headers used by this project. 45 | // Libs for windows.h 46 | #pragma comment(lib, "gdi32.lib") 47 | #pragma comment(lib, "user32.lib") 48 | #pragma comment(lib, "ole32.lib") 49 | 50 | #pragma comment(lib, "d3d11.lib") 51 | #include 52 | #pragma comment(lib, "dxgi.lib") 53 | #include 54 | #pragma comment(lib, "dwmapi.lib") 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include "../screenscraper.h" 60 | #pragma comment(lib, "shell32.lib") 61 | #include 62 | #include 63 | #pragma comment(lib, "opengl32.lib") 64 | 65 | #endif // WLB_WIN_STDAFX_H_ 66 | -------------------------------------------------------------------------------- /src/x11/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include 18 | #include "../clioptions.h" 19 | 20 | void run_server(clioptions *opts); 21 | 22 | int main(int argc, const char **argv) 23 | { 24 | clioptions opts; 25 | parse_commandline(argc, argv, &opts); 26 | run_server(&opts); 27 | return 0; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /src/x11/screenscraper.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Google Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #include "../screenscraper.h" 18 | #include "../latency-benchmark.h" 19 | #include 20 | #include // XGetPixel, XDestroyImage 21 | #include // XK_Z 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include // gettimeofday 27 | #include // memset 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | 41 | 42 | static Display *display = NULL; 43 | 44 | #define min(X, Y) ((X) < (Y) ? (X) : (Y)) 45 | #define max(X, Y) ((X) > (Y) ? (X) : (Y)) 46 | 47 | 48 | static int clamp(int value, int minimum, int maximum) { 49 | return max(min(value, maximum), minimum); 50 | } 51 | 52 | 53 | screenshot *take_screenshot(uint32_t x, uint32_t y, uint32_t width, 54 | uint32_t height) { 55 | if (!display) { 56 | display = XOpenDisplay(NULL); 57 | if (!display) { 58 | return false; 59 | } 60 | } 61 | // Make sure width and height can be safely converted to signed integers. 62 | width = min(width, INT_MAX); 63 | height = min(height, INT_MAX); 64 | int display_x, display_y; 65 | unsigned int display_width, display_height, border_width, display_depth; 66 | Window root; 67 | XGetGeometry(display, RootWindow(display, 0), &root, &display_x, &display_y, 68 | &display_width, &display_height, &border_width, &display_depth); 69 | // TODO: can these be non-zero? 70 | assert(display_x == 0); 71 | assert(display_y == 0); 72 | x = clamp(x, 0, display_width); 73 | y = clamp(y, 0, display_width); 74 | int clamped_width = clamp(width, 0, display_width - x); 75 | int clamped_height = clamp(height, 0, display_height - y); 76 | if (clamped_width == 0 || clamped_height == 0) { 77 | debug_log("screenshot rect empty"); 78 | return NULL; 79 | } 80 | XImage *image = XGetImage(display, RootWindow(display, 0), x, y, 81 | clamped_width, clamped_height, AllPlanes, ZPixmap); 82 | assert(image); 83 | assert(image->width == clamped_width); 84 | assert(image->height == clamped_height); 85 | assert(image->byte_order == LSBFirst); 86 | assert(image->bits_per_pixel == 32); 87 | assert(image->red_mask == 0x00FF0000); 88 | assert(image->green_mask == 0x0000FF00); 89 | assert(image->blue_mask == 0x000000FF); 90 | screenshot *shot = (screenshot *) malloc(sizeof(screenshot)); 91 | shot->width = image->width; 92 | shot->height = image->height; 93 | shot->stride = image->bytes_per_line; 94 | shot->pixels = (uint8_t *)image->data; 95 | shot->time_nanoseconds = get_nanoseconds(); 96 | shot->platform_specific_data = image; 97 | return shot; 98 | } 99 | 100 | 101 | void free_screenshot(screenshot *shot) { 102 | XDestroyImage((XImage *)shot->platform_specific_data); 103 | free(shot); 104 | } 105 | 106 | 107 | static bool send_keystroke(int keysym) { 108 | if (!display) { 109 | display = XOpenDisplay(NULL); 110 | if (!display) { 111 | return false; 112 | } 113 | } 114 | // Send a keydown event for the 'Z' key, followed immediately by keyup. 115 | XKeyEvent event; 116 | memset(&event, 0, sizeof(XKeyEvent)); 117 | Window focused; 118 | int ignored; 119 | XGetInputFocus(display, &focused, &ignored); 120 | event.display = display; 121 | event.root = RootWindow(display, 0); 122 | event.window = focused; 123 | event.subwindow = None; 124 | event.time = CurrentTime; 125 | event.same_screen = True; 126 | event.keycode = XKeysymToKeycode(display, keysym); 127 | event.type = KeyPress; 128 | XSendEvent(display, focused, True, KeyPressMask, (XEvent*) &event); 129 | event.type = KeyRelease; 130 | XSendEvent(display, focused, True, KeyReleaseMask, (XEvent*) &event); 131 | XSync(display, False); 132 | return true; 133 | } 134 | 135 | 136 | bool send_keystroke_b() { return send_keystroke(XK_B); } 137 | bool send_keystroke_t() { return send_keystroke(XK_T); } 138 | bool send_keystroke_w() { return send_keystroke(XK_W); } 139 | bool send_keystroke_z() { return send_keystroke(XK_Z); } 140 | 141 | 142 | bool send_scroll_down(int x, int y) { 143 | if (!display) { 144 | display = XOpenDisplay(NULL); 145 | if (!display) { 146 | return false; 147 | } 148 | } 149 | XWarpPointer(display, None, RootWindow(display, 0), 0, 0, 0, 0, x, y); 150 | static bool x_test_extension_queried = false; 151 | static bool x_test_extension_available = false; 152 | if (!x_test_extension_queried) { 153 | x_test_extension_queried = true; 154 | int ignored; 155 | x_test_extension_available = XTestQueryExtension(display, &ignored, 156 | &ignored, &ignored, &ignored); 157 | } 158 | if (!x_test_extension_available) { 159 | debug_log("XTest extension not available."); 160 | return false; 161 | // TODO: figure out why XSendEvent isn't working. XTest shouldn't be 162 | // required. 163 | // XButtonEvent event; 164 | // memset(&event, 0, sizeof(XButtonEvent)); 165 | // Window focused; 166 | // int ignored; 167 | // XGetInputFocus(display, &focused, &ignored); 168 | // event.display = display; 169 | // event.root = RootWindow(display, 0); 170 | // event.window = focused; 171 | // event.subwindow = None; 172 | // event.time = CurrentTime; 173 | // event.same_screen = True; 174 | // event.type = ButtonPress; 175 | // // TODO: calculate these correctly, they should be relative to the 176 | // // focused window 177 | // event.x = x; 178 | // event.y = y; 179 | // event.x_root = 1; 180 | // event.y_root = 1; 181 | // event.button = Button5; 182 | // XSendEvent(display, focused, True, ButtonPressMask, (XEvent*) &event); 183 | // event.state = Button5Mask; 184 | // event.type = ButtonRelease; 185 | // XSendEvent(display, focused, True, ButtonReleaseMask, (XEvent*) &event); 186 | } 187 | XTestFakeButtonEvent(display, Button5, true, CurrentTime); 188 | XTestFakeButtonEvent(display, Button5, false, CurrentTime); 189 | XSync(display, False); 190 | return true; 191 | } 192 | 193 | 194 | static bool start_time_initialized = false; 195 | static struct timeval start_time; 196 | static const int64_t nanoseconds_per_microsecond = 1000; 197 | int64_t get_nanoseconds() { 198 | if (!start_time_initialized) { 199 | gettimeofday(&start_time, NULL); 200 | start_time_initialized = true; 201 | } 202 | struct timeval current_time, difference; 203 | gettimeofday(¤t_time, NULL); 204 | timersub(¤t_time, &start_time, &difference); 205 | return ((int64_t)difference.tv_sec) * nanoseconds_per_second + 206 | ((int64_t)difference.tv_usec) * nanoseconds_per_microsecond; 207 | } 208 | 209 | 210 | void debug_log(const char *message, ...) { 211 | #ifndef NDEBUG 212 | va_list list; 213 | va_start(list, message); 214 | vprintf(message, list); 215 | va_end(list); 216 | putchar('\n'); 217 | fflush(stdout); 218 | #endif 219 | } 220 | 221 | static pid_t browser_process_pid = 0; 222 | 223 | bool open_browser(const char *program, const char *args, const char *url) { 224 | assert(url); 225 | if (browser_process_pid) { 226 | debug_log("Warning: calling open_browser, but browser already open."); 227 | } 228 | if (program == NULL) { 229 | program = "xdg-open"; 230 | } 231 | if (args == NULL) { 232 | args = ""; 233 | } 234 | 235 | char command_line[4096]; 236 | snprintf(command_line, sizeof(command_line), "'%s' %s '%s'", program, args, url); 237 | command_line[sizeof(command_line) - 1] = '\0'; 238 | 239 | wordexp_t expanded_args; 240 | int result = wordexp(command_line, &expanded_args, 0); 241 | if (result) { 242 | debug_log("Failed to parse command line: %s", command_line); 243 | return false; 244 | } 245 | browser_process_pid = fork(); 246 | if (!browser_process_pid) { 247 | // child process, launch the browser! 248 | execvp(expanded_args.we_wordv[0], expanded_args.we_wordv); 249 | debug_log("Failed to execute browser!"); 250 | exit(1); 251 | } 252 | wordfree(&expanded_args); 253 | return true; 254 | } 255 | 256 | bool close_browser() { 257 | if (browser_process_pid == 0) { 258 | debug_log("Browser not open"); 259 | return false; 260 | } 261 | int r = kill(browser_process_pid, SIGKILL); 262 | browser_process_pid = 0; 263 | if (r) { 264 | debug_log("Failed to close browser window"); 265 | return false; 266 | } 267 | return true; 268 | } 269 | 270 | static bool extension_supported(const char *name) { 271 | const char *extensions = glXQueryExtensionsString(display, DefaultScreen(display)); 272 | debug_log(extensions); 273 | const char *found = strstr(extensions, name); 274 | if (found) { 275 | debug_log("found"); 276 | char end = found[strlen(name)]; 277 | if (end == '\0' || end == ' ') { 278 | return true; 279 | } 280 | } 281 | return false; 282 | } 283 | 284 | 285 | typedef int (*glXSwapIntervalMESA_t)(int); 286 | static glXSwapIntervalMESA_t p_glXSwapIntervalMESA = NULL; 287 | typedef void (*glXSwapIntervalEXT_t)(Display *, GLXDrawable, int); 288 | static glXSwapIntervalEXT_t p_glXSwapIntervalEXT = NULL; 289 | 290 | 291 | static void initialize_gl_extensions() { 292 | if (extension_supported("GLX_MESA_swap_control")) { 293 | // Intel and AMD and MESA support this one, but not NVIDIA 294 | p_glXSwapIntervalMESA = (glXSwapIntervalMESA_t)glXGetProcAddressARB((const GLubyte *)"glXSwapIntervalMESA"); 295 | } 296 | if (extension_supported("GLX_EXT_swap_control")) { 297 | // This one is supported by NVIDIA, but not Intel or AMD 298 | p_glXSwapIntervalEXT = (glXSwapIntervalEXT_t)glXGetProcAddressARB((const GLubyte *)"glXSwapIntervalEXT"); 299 | } 300 | } 301 | 302 | 303 | static void native_reference_window_event_loop(uint8_t pattern[]) { 304 | // This function should only be called from a child process that isn't yet 305 | // connected to the X server. 306 | assert(!display); 307 | display = XOpenDisplay(NULL); 308 | assert(display); 309 | // Initialize GLX. 310 | int visual_attributes[] = { GLX_RGBA, 311 | GLX_DOUBLEBUFFER, 312 | GLX_RED_SIZE, 1, 313 | GLX_GREEN_SIZE, 1, 314 | GLX_BLUE_SIZE, 1, 315 | None, 316 | }; 317 | XVisualInfo *xvi = glXChooseVisual(display, DefaultScreen(display), 318 | visual_attributes); 319 | assert(xvi); 320 | GLXContext context = glXCreateContext(display, xvi, NULL, true); 321 | assert(context); 322 | if (!context) { 323 | debug_log("failed to initialize OpenGL"); 324 | exit(1); 325 | } 326 | 327 | // Create a window with the correct colormap for GL rendering. 328 | Colormap colormap = XCreateColormap(display, RootWindow(display, 0), 329 | xvi->visual, AllocNone); 330 | XSetWindowAttributes xswa; 331 | memset(&xswa, 0, sizeof(xswa)); 332 | xswa.colormap = colormap; 333 | // Prevent the window manager from moving this window or putting decorations 334 | // on it. 335 | xswa.override_redirect = true; 336 | Window window = XCreateWindow(display, RootWindow(display, 0), 500, 500, 337 | pattern_pixels, 1, 0, xvi->depth, InputOutput, 338 | xvi->visual, CWColormap | CWOverrideRedirect, 339 | &xswa); 340 | assert(window); 341 | 342 | XmbSetWMProperties(display, window, "Test window", NULL, NULL, 0, NULL, NULL, 343 | NULL); 344 | XSelectInput(display, window, KeyPressMask | ButtonPressMask | ExposureMask); 345 | 346 | // Initialize GL and extensions. 347 | bool success = glXMakeCurrent(display, window, context); 348 | assert(success); 349 | initialize_gl_extensions(); 350 | 351 | // Disable vsync to avoid blocking on swaps. Ideally we would sync to the 352 | // display's refresh rate and render at the best possible time to achieve low 353 | // latency and low CPU use while still avoiding tearing. That should be 354 | // possible, but will be difficult to implement and will depend on driver/ 355 | // compositor support. For now we'll just render as fast as possible to 356 | // achieve low latency. 357 | bool disabled_vsync = false; 358 | if (p_glXSwapIntervalMESA) { 359 | int ret = p_glXSwapIntervalMESA(0); 360 | if (ret) { 361 | debug_log("glXSwapIntervalMESA failed %d", ret); 362 | exit(1); 363 | } 364 | disabled_vsync = true; 365 | } 366 | if (!disabled_vsync && p_glXSwapIntervalEXT) { 367 | p_glXSwapIntervalEXT(display, window, 0); 368 | disabled_vsync = true; 369 | } 370 | if (!disabled_vsync) { 371 | debug_log("No method of disabling vsync available."); 372 | exit(1); 373 | } 374 | 375 | // Draw the pattern on the window before showing it. 376 | int scrolls = 0; 377 | int key_downs = 0; 378 | int esc_presses = 0; 379 | draw_pattern_with_opengl(pattern, scrolls, key_downs, esc_presses); 380 | glXSwapBuffers(display, window); 381 | 382 | // Show the window. 383 | XMapRaised(display, window); 384 | // Override-redirect windows don't automatically gain focus when mapped, so we 385 | // have to steal it manually. 386 | XSetInputFocus(display, window, RevertToParent, CurrentTime); 387 | 388 | // Process X11 events in a loop forever unless the parent process dies. 389 | while (getppid() != 1) { 390 | while (XPending(display)) { 391 | XEvent event; 392 | XNextEvent(display, &event); 393 | if (event.type == ButtonPress) { 394 | // This is probably a mousewheel event. 395 | scrolls++; 396 | } else if (event.type == KeyPress) { 397 | if (XkbKeycodeToKeysym(display, event.xkey.keycode, 0, 0) == 398 | XK_Escape) { 399 | esc_presses++; 400 | } 401 | key_downs++; 402 | } 403 | } 404 | draw_pattern_with_opengl(pattern, scrolls, key_downs, esc_presses); 405 | glXSwapBuffers(display, window); 406 | usleep(1000 * 5); 407 | } 408 | XCloseDisplay(display); 409 | } 410 | 411 | 412 | static pid_t window_process_pid = 0; 413 | 414 | 415 | bool open_native_reference_window(uint8_t *test_pattern_for_window) { 416 | if (window_process_pid != 0) { 417 | debug_log("Native reference window already open"); 418 | return false; 419 | } 420 | window_process_pid = fork(); 421 | if (!window_process_pid) { 422 | // Child process. Throw away the X11 display connection from the parent 423 | // process; we will create a new one for the child. 424 | display = NULL; 425 | native_reference_window_event_loop(test_pattern_for_window); 426 | exit(0); 427 | } 428 | // Parent process. Wait for the child to launch and show its window before 429 | // returning. 430 | usleep(1000000 /* 1 second */); 431 | return true; 432 | } 433 | 434 | bool close_native_reference_window() { 435 | if (window_process_pid == 0) { 436 | debug_log("Native reference window not open"); 437 | return false; 438 | } 439 | int r = kill(window_process_pid, SIGKILL); 440 | window_process_pid = 0; 441 | if (r) { 442 | debug_log("Failed to close native reference window"); 443 | return false; 444 | } 445 | return true; 446 | } 447 | --------------------------------------------------------------------------------