├── .gitignore ├── fps-measurement ├── redirect_stdout_to_file.gdb ├── rpms │ ├── gdb-7.2-2.1.armv7l.rpm │ ├── gdb-7.2-2.1.i586.rpm │ ├── xmacro-pre0.3_reaktor-1.i586.rpm │ └── xmacro-pre0.3_reaktor-1.armv7l.rpm ├── example_testcases │ ├── ScrollingPerformance.wgt │ ├── goto_tab_2.xmacro │ ├── goto_tab_3.xmacro │ ├── test_scrolling_translate3d.fpstest │ ├── test_scrolling_overflow-scroll.fpstest │ ├── test_scrolling_webkit-overflow-scrolling-touch.fpstest │ └── scroll_up_and_down.xmacro ├── fps-stats ├── fps-meter.js ├── macro_recorder.js ├── generate_linear_macro ├── generate_sine_macro ├── README.md └── fpstest ├── example ├── files.json ├── package.json └── Gruntfile.js ├── .jshintrc ├── tizendev ├── Gruntfile.js ├── package.json ├── LICENSE.md ├── tasks ├── lib │ ├── profiling.js │ ├── util.js │ ├── shell.js │ └── tasks.js └── tizendev.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | tmp 4 | tizendevbuild -------------------------------------------------------------------------------- /fps-measurement/redirect_stdout_to_file.gdb: -------------------------------------------------------------------------------- 1 | call dup2(open("/tmp/fpslog", 01101), 1) 2 | detach 3 | quit 4 | -------------------------------------------------------------------------------- /fps-measurement/rpms/gdb-7.2-2.1.armv7l.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktor/tizendev/HEAD/fps-measurement/rpms/gdb-7.2-2.1.armv7l.rpm -------------------------------------------------------------------------------- /fps-measurement/rpms/gdb-7.2-2.1.i586.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktor/tizendev/HEAD/fps-measurement/rpms/gdb-7.2-2.1.i586.rpm -------------------------------------------------------------------------------- /fps-measurement/rpms/xmacro-pre0.3_reaktor-1.i586.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktor/tizendev/HEAD/fps-measurement/rpms/xmacro-pre0.3_reaktor-1.i586.rpm -------------------------------------------------------------------------------- /fps-measurement/rpms/xmacro-pre0.3_reaktor-1.armv7l.rpm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktor/tizendev/HEAD/fps-measurement/rpms/xmacro-pre0.3_reaktor-1.armv7l.rpm -------------------------------------------------------------------------------- /fps-measurement/example_testcases/ScrollingPerformance.wgt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reaktor/tizendev/HEAD/fps-measurement/example_testcases/ScrollingPerformance.wgt -------------------------------------------------------------------------------- /example/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "js": [ 3 | "lib/jquery-1.7.1.js", 4 | "lib/backbone.js", 5 | "app/app.js", 6 | ], 7 | "css": [ 8 | "css/reset.css", 9 | "css/header.css", 10 | "css/main.css" 11 | ] 12 | } -------------------------------------------------------------------------------- /fps-measurement/example_testcases/goto_tab_2.xmacro: -------------------------------------------------------------------------------- 1 | #this_is_a_comment._whitespace_is_not_supported_in_comments 2 | 3 | #click_on_tab_2 4 | MotionNotify 590 124 5 | ButtonPress 1 6 | ButtonRelease 1 7 | Delay 0.5 #wait_for_page_to_load 8 | -------------------------------------------------------------------------------- /fps-measurement/example_testcases/goto_tab_3.xmacro: -------------------------------------------------------------------------------- 1 | #this_is_a_comment._whitespace_is_not_supported_in_comments 2 | 3 | #click_on_tab_3 4 | MotionNotify 670 124 5 | ButtonPress 1 6 | ButtonRelease 1 7 | Delay 0.5 #wait_for_page_to_load 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /tizendev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | FOLDER=`pwd` 4 | 5 | BIN="$0" 6 | while [ -h $BIN ]; do 7 | BIN=$(readlink $BIN) 8 | done 9 | RUNDIR=`dirname $BIN` 10 | 11 | cd $RUNDIR 12 | CMD=$1 13 | shift 14 | grunt tizendev:$CMD --sourceDir=$FOLDER --buildPath=$FOLDER/tizendevbuild $@ 15 | 16 | cd $FOLDER 17 | -------------------------------------------------------------------------------- /fps-measurement/fps-stats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/awk -f 2 | BEGIN { FS = "[ ,]+" } 3 | /^FRAME: / { sum += data[NR] = $4 } 4 | END { 5 | avg = sum / NR 6 | for (i in data) { 7 | diff = data[i] - avg 8 | sqsum += diff * diff 9 | } 10 | stddev = sqrt(sqsum / (NR - 1)) 11 | 12 | printf "FPS: %.1f ± %.2f (N=%d)\n", avg, stddev, NR 13 | } 14 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "grunt": "~0.4.1", 4 | "grunt-contrib-watch": "~0.5.3", 5 | "grunt-contrib-copy": "~0.4.1", 6 | "grunt-tizen": "~0.1.1", 7 | "grunt-contrib-uglify": "0.2.4", 8 | "grunt-contrib-cssmin": "0.6.2" 9 | }, 10 | "peerDependencies": { 11 | "grunt": "~0.4.1" 12 | }, 13 | "keywords": [ 14 | "tizen gruntplugin" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /fps-measurement/example_testcases/test_scrolling_translate3d.fpstest: -------------------------------------------------------------------------------- 1 | #This file is bourne shell script that is sourced sourced by fpstest 2 | 3 | APPID=6nqks5opL0.ScrollingPerformance #Mandatory 4 | 5 | # This function defines what happens in the test case 6 | # Available functions: 7 | # run_xmacro run an xmacro script 8 | # start_fps_log clear the FPS log to measure only the correct stuff 9 | testcase() { 10 | sleep 2 # wait for application to start 11 | start_fps_log 12 | run_xmacro scroll_up_and_down.xmacro 13 | } 14 | -------------------------------------------------------------------------------- /fps-measurement/example_testcases/test_scrolling_overflow-scroll.fpstest: -------------------------------------------------------------------------------- 1 | #This file is bourne shell script that is sourced by fpstest 2 | 3 | APPID=6nqks5opL0.ScrollingPerformance #Mandatory 4 | 5 | # This function defines what happens in the test case 6 | # Available functions: 7 | # run_xmacro run an xmacro script 8 | # start_fps_log clear the FPS log to measure only the correct stuff 9 | testcase() { 10 | sleep 2 # wait for application to start 11 | run_xmacro goto_tab_3.xmacro 12 | start_fps_log 13 | run_xmacro scroll_up_and_down.xmacro 14 | } 15 | -------------------------------------------------------------------------------- /fps-measurement/fps-meter.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame; 3 | 4 | var start = Date.now(), frames = 0; 5 | 6 | var callback = function() { 7 | frames++; 8 | requestAnimationFrame(callback); 9 | }; 10 | 11 | requestAnimationFrame(callback); 12 | 13 | window.reset = function() { 14 | var time = (Date.now()-start)/1000; 15 | console.log("Average FPS: " + (frames/time) + " - Frames: " + frames + " - Time: " + time + "s"); 16 | start = Date.now(); 17 | frames = 0; 18 | }; 19 | })(this); -------------------------------------------------------------------------------- /fps-measurement/example_testcases/test_scrolling_webkit-overflow-scrolling-touch.fpstest: -------------------------------------------------------------------------------- 1 | #This file is bourne shell script that is sourced sourced by fpstest 2 | 3 | APPID=6nqks5opL0.ScrollingPerformance #Mandatory 4 | 5 | # This function defines what happens in the test case 6 | # Available functions: 7 | # run_xmacro run an xmacro script 8 | # start_fps_log clear the FPS log to measure only the correct stuff 9 | testcase() { 10 | sleep 2 # wait for application to start 11 | run_xmacro goto_tab_2.xmacro 12 | start_fps_log 13 | run_xmacro scroll_up_and_down.xmacro 14 | } 15 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Project configuration. 6 | grunt.initConfig({ 7 | 8 | /* Configure the plugin so that it can be used through tizendev shell command */ 9 | tizendev: { 10 | profilePath: "~/workspace/.metadata/.plugins/org.tizen.common.sign/profiles.xml", 11 | tasks: { 12 | } 13 | }, 14 | 15 | jshint: { 16 | options: { 17 | eqnull: true, 18 | sub: true 19 | }, 20 | all: ["tasks/**/*.js"] 21 | } 22 | }); 23 | 24 | // Load dependencies 25 | grunt.loadNpmTasks('grunt-contrib-watch'); 26 | grunt.loadNpmTasks("grunt-contrib-copy"); 27 | grunt.loadNpmTasks("grunt-tizen"); 28 | 29 | // Load plugin 30 | grunt.loadTasks('tasks'); 31 | 32 | //Jshint 33 | grunt.loadNpmTasks('grunt-contrib-jshint'); 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tizendev", 3 | "private": true, 4 | "description": "Tizen web app development tools for grunt", 5 | "version": "0.3.0", 6 | "homepage": "", 7 | "author": { 8 | "name": "Reaktor" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "gitti:grunt-fulari.git" 13 | }, 14 | "bugs": { 15 | "url": "" 16 | }, 17 | "main": "Gruntfile.js", 18 | "engines": { 19 | "node": ">= 0.8.0" 20 | }, 21 | "dependencies": { 22 | "q": "~0.9.4", 23 | "baconjs": "~0.6.19", 24 | "xml2js": "0.2.8" 25 | }, 26 | "devDependencies": { 27 | "grunt-contrib-watch": "~0.5.3", 28 | "grunt": "~0.4.1", 29 | "grunt-tizen": "~0.1.1", 30 | "grunt-contrib-copy": "~0.4.1", 31 | "grunt-contrib-jshint": "~0.6.4" 32 | }, 33 | "peerDependencies": { 34 | "grunt": "~0.4.1" 35 | }, 36 | "keywords": [ 37 | "tizen gruntplugin" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Reaktor Innovations Oy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /fps-measurement/macro_recorder.js: -------------------------------------------------------------------------------- 1 | (function(window) { 2 | var $document = $(document); 3 | var time, released = true, logContent = "", prevX, prevY; 4 | 5 | function logTouchPosition(event) { 6 | prevX = event.touches[0].screenX; 7 | prevY = event.touches[0].screenY; 8 | log("MotionNotify", prevX, prevY); 9 | } 10 | 11 | function log() { 12 | logContent += [].slice.call(arguments, 0).join(" ") + "\n"; 13 | } 14 | 15 | function logDelays() { 16 | var currentTime = (new Date()).getTime(), delay; 17 | if (time && (delay = (currentTime - time) / 1000) > 0) { 18 | log("Delay", delay); 19 | } 20 | 21 | time = currentTime; 22 | } 23 | 24 | function release() { 25 | released = true; 26 | log('ButtonRelease 1'); 27 | } 28 | 29 | function interpolate(x1, y1, x2, y2, duration) { 30 | var total = Math.floor(duration / 10), 31 | count = 0; 32 | while(duration > 10) { 33 | prevX = x1 - Math.round(((x1-x2)/total) * count); 34 | prevY = y1 - Math.round(((y1-y2)/total) * count); 35 | log("MotionNotify", prevX, prevY); 36 | log("Delay", 0.010); 37 | count++; 38 | duration -= 10; 39 | } 40 | 41 | } 42 | 43 | document.addEventListener('touchstart', function(event) { 44 | if (!released) { 45 | release(); 46 | } 47 | released = false; 48 | logDelays(); 49 | logTouchPosition(event); 50 | log('ButtonPress 1'); 51 | }, false); 52 | 53 | document.addEventListener('touchmove', function(event) { 54 | var currentTime = (new Date()).getTime(); 55 | interpolate(prevX, prevY, event.touches[0].screenX, event.touches[0].screenY, currentTime - time); 56 | time = currentTime; 57 | }, false); 58 | 59 | document.addEventListener('touchend touchcancel touchleave', release, false); 60 | 61 | window.stopRecord = function() { 62 | if (!released) { 63 | release(); 64 | } 65 | console.log(logContent); 66 | logContent = ""; 67 | return "Recording ended, reset log"; 68 | }; 69 | 70 | console.log("Recording started..."); 71 | })(this); 72 | -------------------------------------------------------------------------------- /fps-measurement/generate_linear_macro: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import argparse 5 | 6 | def parse_arguments(): 7 | parser = argparse.ArgumentParser( 8 | description="Generate xmacro with sine-motion along one axis.") 9 | parser.add_argument('x1', help="startpoint x-coord", type=int) 10 | parser.add_argument('y1', help="startpoint y-coord", type=int) 11 | parser.add_argument('x2', help="endpoint x-coord", type=int) 12 | parser.add_argument('y2', help="endpoint y-coord", type=int) 13 | parser.add_argument('-t', '--tick', 14 | help="tick between events in seconds (0.010)", 15 | type=float, default=0.010) 16 | parser.add_argument('-e', '--end-delay', 17 | help="duration to wait after the end of motion (0s)", 18 | type=float, default=0.0) 19 | parser.add_argument('-d', '--duration', 20 | help="duration of the movement in seconds (1s)", type=float, default=1.0) 21 | parser.add_argument('-r', '--repeat', 22 | help="How many times to repeat the whole thing (1)", 23 | type=int, default=1) 24 | 25 | args = parser.parse_args() 26 | 27 | args.number_of_ticks = int(args.duration / args.tick) 28 | 29 | return args 30 | 31 | def calculate_coordinates(args, tick): 32 | 33 | x = round(args.x1 + (args.x2 - args.x1)*(float(tick)/args.number_of_ticks)) 34 | y = round(args.y1 + (args.y2 - args.y1)*(float(tick)/args.number_of_ticks)) 35 | return x, y 36 | 37 | def generate_macro(args): 38 | print "MotionNotify %d %d" % calculate_coordinates(args, 0) 39 | print "ButtonPress 1" 40 | 41 | for tick in range(1, args.number_of_ticks + 1): 42 | x, y = calculate_coordinates(args, tick) 43 | print "MotionNotify %d %d" % (x, y) 44 | print "Delay %.3f" % args.tick 45 | 46 | print "ButtonRelease 1" 47 | if args.end_delay > 0.0: 48 | print "Delay %.3f" % args.end_delay 49 | 50 | 51 | def main(): 52 | args = parse_arguments() 53 | 54 | print "#generated_with:_" + "_".join(sys.argv) 55 | for i in range(args.repeat): 56 | generate_macro(args) 57 | 58 | main() 59 | -------------------------------------------------------------------------------- /example/Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Real life example 4 | 5 | module.exports = function(grunt) { 6 | 7 | var files = grunt.file.readJSON('./files.json'); 8 | 9 | grunt.initConfig({ 10 | tizendev: { 11 | profilePath: "~/workspace/.metadata/.plugins/org.tizen.common.sign/profiles.xml", 12 | 13 | /* Run uglify and cssmin when a source file changes */ 14 | tasks: { 15 | uglifyTask: ["app/**/*.js"], 16 | cssmin: ["css/**/*.css"] 17 | } 18 | }, 19 | 20 | // configuration for uglify plugin 21 | uglify: { 22 | // production target: minimize the files 23 | prod: { 24 | options: { 25 | compress: true, 26 | beautify: false, 27 | mangle: false, 28 | preserveComments: 'some' 29 | }, 30 | files: { 31 | '<%=tizendev.buildPath%>/app/app-minified.js': files.js 32 | } 33 | }, 34 | 35 | // dev target: concat the files and create source maps 36 | dev: { 37 | options: { 38 | sourceMapRoot: '', 39 | sourceMap: 'tizendevbuild/app/app-minified.js.map', 40 | sourceMappingURL: 'app-minified.js.map', 41 | compress: false, 42 | mangle: false, 43 | beautify: true 44 | }, 45 | files: { 46 | '<%=tizendev.buildPath%>/app/app-minified.js': files.js 47 | } 48 | } 49 | }, 50 | 51 | // configuration for cssmin plugin 52 | cssmin: { 53 | combine: { 54 | files: { 55 | '<%=tizendev.buildPath%>/css/style-minified.css': files.css 56 | } 57 | } 58 | } 59 | 60 | }); 61 | 62 | // Load dependencies for Tizendev 63 | grunt.loadNpmTasks('grunt-contrib-watch'); 64 | grunt.loadNpmTasks("grunt-contrib-copy"); 65 | grunt.loadNpmTasks("grunt-tizen"); 66 | 67 | // Load Tizendev plugin 68 | grunt.loadNpmTasks('tizendev'); 69 | 70 | // Load additional tasks 71 | grunt.loadNpmTasks("grunt-contrib-uglify"); 72 | grunt.loadNpmTasks("grunt-contrib-cssmin"); 73 | 74 | // Run uglify:prod or uglify:dev target depending on 'usemin' command-line argument. 75 | var uglifyTask = "uglify:prod"; 76 | if(grunt.option('usemin') === false){ 77 | uglifyTask = "uglify:dev"; 78 | } 79 | grunt.registerTask("uglifyTask", [uglifyTask]); 80 | }; 81 | -------------------------------------------------------------------------------- /fps-measurement/generate_sine_macro: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import math 5 | import argparse 6 | 7 | def parse_arguments(): 8 | parser = argparse.ArgumentParser( 9 | description="Generate xmacro with sine-motion along one axis.") 10 | parser.add_argument('-t', '--tick', 11 | help="tick between events in seconds (0.010)", 12 | type=float, default=0.010) 13 | parser.add_argument('-p', '--periods', 14 | help="number of repeats (10.0)", type=float, default=10.0) 15 | parser.add_argument('-f', '--freq', 16 | help="Frequency (speed) (1.0 Hz)", type=float, default=1.0) 17 | parser.add_argument('-a', '--amplitude', 18 | help="Peak amplitude of movement (400 px)", type=int, default=400) 19 | parser.add_argument('-x', '--axis', 20 | help="The axis to vary (x)", 21 | type=str, choices=["x","y"], default='y') 22 | parser.add_argument('-c', '--center', 23 | help="Center coordinate of the varied axis (600 px)", 24 | type=int, default=600) 25 | parser.add_argument('-o', '--other-axis-coord', 26 | help="Coordinate of the constant axis (360 px)", 27 | type=int, default=360) 28 | parser.add_argument('-s', '--start-at', 29 | help="Where to start the movement (max)", 30 | type=str, choices={"min", "center_inc", "center_dec", "max"}, default=360) 31 | parser.add_argument('-r', '--repeat', 32 | help="How many times to repeat the whole thing (1)", 33 | type=int, default=1) 34 | 35 | args = parser.parse_args() 36 | 37 | args.phase_shift = 0 # center_inc 38 | if args.start_at == "max": 39 | args.phase_shift = math.pi/2 40 | elif args.start_at == "center_dec": 41 | args.phase_shift = math.pi 42 | elif args.start_at == "min": 43 | args.phase_shift = 3*math.pi/2 44 | 45 | return args 46 | 47 | def calculate_coordinates(args, angle): 48 | x = args.other_axis_coord 49 | y = args.center + round(args.amplitude*math.sin(angle)) 50 | if args.axis == "x": 51 | x, y = y, x 52 | return x, y 53 | 54 | def frange_inclusive(x, y, jump): 55 | while x <= y: 56 | yield x 57 | x += jump 58 | 59 | def generate_macro(args): 60 | print "MotionNotify %d %d" % calculate_coordinates(args, 0+args.phase_shift) 61 | print "ButtonPress 1" 62 | 63 | radian_increment = 2*math.pi*args.freq*args.tick 64 | end_radian = 2*math.pi*args.periods 65 | for angle in frange_inclusive(0.0, end_radian, radian_increment): 66 | x, y = calculate_coordinates(args, angle + args.phase_shift) 67 | print "MotionNotify %d %d" % (x, y) 68 | print "Delay %.3f" % args.tick 69 | 70 | print "ButtonRelease 1" 71 | 72 | def main(): 73 | args = parse_arguments() 74 | 75 | print "#generated_with:_" + "_".join(sys.argv) 76 | for i in range(args.repeat): 77 | generate_macro(args) 78 | 79 | main() 80 | -------------------------------------------------------------------------------- /tasks/lib/profiling.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | "use strict"; 3 | var _ = grunt.util._; 4 | var util = require("./util.js")(grunt); 5 | var shell = require("./shell.js")(grunt); 6 | var Q = require("q"); 7 | 8 | var profiling = { 9 | printProfilingRow: function(row) { 10 | _.each(getProfilingEventNames(), function(eventName) { 11 | grunt.log.write(eventName + " " + row[eventName] + " ms | "); 12 | }); 13 | grunt.log.writeln(); 14 | return row; 15 | }, 16 | 17 | printProfilingResult: function (rows) { 18 | grunt.log.writeln("Averages: (n=" + rows.length + ")"); 19 | var columns = rowsToColumns(rows); 20 | _.each(columns, function(column) { 21 | var i = average(column.data); 22 | grunt.log.write(column.name + " "); 23 | grunt.log.write(Math.round(i.mean) + " ± " + Math.round(i.deviation) + " ms | "); 24 | }); 25 | grunt.log.writeln(""); 26 | }, 27 | 28 | waitDLogEvents: function() { 29 | var deferred = Q.defer(); 30 | var child = shell.getDlogProcess(); 31 | var events = util.applyGruntTemplate(getConfig().profilingEvents); 32 | 33 | function mapEvent(line) { 34 | var evtName = _.find(events, function(eventName) { return line.indexOf(eventName) >= 0; }); 35 | if (evtName) 36 | return { event: evtName, line: line }; 37 | else 38 | return null; 39 | } 40 | 41 | function notNull(evt) { 42 | return evt != null; 43 | } 44 | 45 | shell.readChildProcessOutput(child) 46 | .map(mapEvent) 47 | .filter(notNull) 48 | .slidingWindow(events.length, events.length) 49 | .take(1) 50 | .map(formatEvents) 51 | .map(calcDifferences) 52 | .doAction(function() { child.kill("SIGKILL"); }) 53 | .onValue(deferred.resolve); 54 | 55 | return deferred.promise; 56 | }, 57 | 58 | getStartEventName: function() { 59 | var profilingEvents = util.applyGruntTemplate(getConfig().profilingEvents); 60 | return profilingEvents[0]; 61 | } 62 | }; 63 | 64 | function rowsToColumns(rows) { 65 | var eventNames = getProfilingEventNames(); 66 | return _.reduce(eventNames, function(memo, colName) { 67 | var column = []; 68 | _.each(rows, function(row) { 69 | column.push(row[colName]); 70 | }); 71 | memo.push({name: colName, data: column}); 72 | return memo; 73 | }, []); 74 | } 75 | 76 | function getProfilingEventNames() { 77 | return _.chain([getConfig().profilingEvents, "TOTAL"]).flatten().rest().value(); 78 | } 79 | 80 | function formatEvents(lines) { 81 | var regexp = /^([^a-zA-Z]+)/; 82 | return _.chain(lines).map(function(evt) { 83 | var match = regexp.exec(evt.line); 84 | if (match != null) 85 | return { event: evt.event, time: new Date(match[0])}; 86 | else 87 | return null; 88 | }).compact().value(); 89 | } 90 | 91 | function average(a) { 92 | var r = {mean: 0, variance: 0, deviation: 0}, t = a.length; 93 | for(var m, s = 0, l = t; l--; s += a[l]); 94 | for(m = r.mean = s / t, l = t, s = 0; l--; s += Math.pow(a[l] - m, 2)); 95 | return r.deviation = Math.sqrt(r.variance = s / t), r; 96 | } 97 | 98 | function calcDifferences(events) { 99 | var result = {}; 100 | for (var i=1; i`. This will now print all X11 31 | events related to the app's window. 32 | 4. write the actual test case that specifies the application ID to test and 33 | macros to run. 34 | * see `example_testcases/test_scrolling_overflow-scroll.fpstest` for an 35 | example 36 | 37 | NOTE: The actual FPS-measuring part of the test case must be at least a few 38 | seconds long to generate enough readings. 39 | 40 | 41 | ## Running test cases 42 | 43 | To run a testcase, attach a target to the host machine in USB debugging mode 44 | and run `fpstest test_case.fpstest`. 45 | 46 | Alternatively, you can run `fpstest path_to_dir_with_one_fpstest`. 47 | 48 | When running the script for the first time on a target, the target will need to 49 | be rebooted before actual measurements can be run. The script will prompt the 50 | user to reboot. After a successful test run, the tool will print the results: 51 | 52 | `FPS: 34.6 ± 2.84 (N=18)` 53 | 54 | This means that the FPS average was 34.6 with a standard deviation of 2.84, and 55 | the number of FPS readings was 18. 56 | 57 | NOTE: Testing is based on playing back X11 events, so make sure that there are 58 | no overlay elements or popups in the way of the test. 59 | 60 | 61 | ## Step-by-step tutorial: running a test case 62 | 63 | This section walks through running actual test cases on an example application. 64 | The application is designed to test FPS performance of different styles of 65 | scrolling. 66 | 67 | 1. Install the example application on a target (emulator or device) 68 | * Connect a device or start the emulator 69 | * Turn on USB debugging in Settings if necessary 70 | * `sdb intall example_testcases/ScrollingPerformance.wgt` 71 | 2. If you prefer, play with the app a bit to get a feeling of what it does. 72 | 3. Run the test cases: 73 | * `./fpstest example_testcases/test_scrolling_translate3d.fpstest` 74 | * NOTE: if this is the first run of fpstest with the target, it must be 75 | rebooted. Run this command again after the reboot. 76 | * When the test case is running, you should see the app scrolling by itself. 77 | * When the test case finishes, fpstest will print a line like `FPS: 34.6 ± 2.84 (N=18)` 78 | * `./fpstest example_testcases/test_scrolling_webkit-overflow-scrolling-touch.fpstest` 79 | * `./fpstest example_testcases/test_scrolling_overflow-scroll.fpstest` 80 | -------------------------------------------------------------------------------- /tasks/tizendev.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var fs = require("fs"); 3 | var Q = require("q"); 4 | 5 | module.exports = function(grunt) { 6 | "use strict"; 7 | 8 | var tasks = require("./lib/tasks.js")(grunt); 9 | var util = require("./lib/util.js")(grunt); 10 | var _ = grunt.util._; 11 | var config = null; 12 | var firstRun = true; 13 | 14 | var defaultConfig = { 15 | watch: ["**", "!node_modules/**"], // Files that trigger tizendev tasks. Should include all files that are monitored by copy and task filters. Build path is excluded automatically. 16 | watchExclude: [], // This is appended to watch - useful for excluding files in project's gruntfile 17 | copy: ["**", "!node_modules/**", "!*.wgt"], // What files to copy to build folder (and sync to phone). 18 | copyExclude: [], // This is appended to copy - useful for excluding files in project's gruntfile 19 | tasks: {}, // tasks to run after file change. for example: tasks: { "uglify": ["js/*.js", "otherfolder/**/*.js"] } 20 | sdkPath: "~/tizen-sdk", 21 | binPath: "<%=tizendev.sdkPath%>/tools/ide/bin/", // location of CLI tools 22 | nativeTarget: "armel", //specify an architecture ("armel" | "i386") 23 | profile: "", // the name of the profile used for signing. If empty, profile name is parsed from profiles.xml 24 | profilePath: "", // location of profiles.xml 25 | sourceDir: ".", // root folder, ie. folder where config.xml is located 26 | buildPath: "tizendevbuild", // build folder, must NOT be project root. 27 | fileChanged: undefined, // custom function to execute after file change. Receives file name as argument. 28 | remoteAppLocation: "/opt/usr/apps/<%=tizendev.appId%>/res/wgt/", // Application directory on tizen device. appId is determined runtime. 29 | profilingEvents: ["launch request : <%=tizendev.fullAppId%>", "getUri(): default uri", "E_PARSED", "E_LOADED"], // Log events that trigger profiling 30 | profilingTimes: 1, // number of profiling attempts 31 | profilingSleep: 5, // seconds between profiling rounds 32 | nativePath: "", // native project that should be linked to a hybrid app 33 | liveReload: true, // enable livereload server when in debug mode 34 | tizenAppScriptDir: "/" //Folder where grunt-tizen dependency puts its' helper script tizen-app.sh 35 | }; 36 | 37 | function getConfig() { 38 | function replaceHomeDir(dir) { 39 | return dir.replace(/^~\//, process.env.HOME + '/'); 40 | } 41 | 42 | if (!config) { 43 | var cmdLineConfig = getCmdLineConfig(); 44 | 45 | config = _.extend(defaultConfig, grunt.config.get("tizendev"), cmdLineConfig); 46 | config.sdkPath = replaceHomeDir(config.sdkPath); 47 | config.binPath = replaceHomeDir(config.binPath); 48 | config.profilePath = replaceHomeDir(config.profilePath); 49 | config.sourceDir = replaceHomeDir(config.sourceDir); 50 | config.fullAppId = util.getAppId(path.join(config.sourceDir, "config.xml")); 51 | config.appId = config.fullAppId.split(".")[0]; 52 | 53 | if (!util.isAbsolutePath(config.buildPath)) 54 | config.buildPath = path.join(config.sourceDir, config.buildPath); 55 | 56 | if (path.resolve(config.sourceDir) == path.resolve(config.buildPath)) 57 | grunt.fail.fatal("Build folder must not be the same as source folder"); 58 | } 59 | 60 | return config; 61 | } 62 | 63 | function getCmdLineConfig() { 64 | return _.reduce(defaultConfig, function(memo, value, key) { 65 | var arg = grunt.option(key); 66 | 67 | if (arg != null) { 68 | if (_.isString(value)) 69 | memo[key] = arg; 70 | else if (_.isNumber(value)) { 71 | memo[key] = parseInt(arg, 10); 72 | } else if (_.isArray(value)) { 73 | memo[key] = arg.split(","); 74 | } else if (_.isBoolean(value)) { 75 | memo[key] = arg; 76 | } 77 | } 78 | return memo; 79 | }, {}); 80 | } 81 | 82 | grunt.registerTask("tizendev", function(cmd) { 83 | grunt.config("tizendev", getConfig()); 84 | grunt.config.set("tizen_configuration.configFile", path.join(getConfig().sourceDir, "config.xml")); 85 | grunt.config.set("tizen_configuration.tizenAppScriptDir", getConfig().tizenAppScriptDir); 86 | if (!cmd) { 87 | grunt.fail.warn("Tizendev task parameter not specified. Possible options: " + Object.keys(tasks).join(", ") + "\n"); 88 | } 89 | 90 | if (firstRun) { 91 | console.log("Configuration:", config); 92 | console.log("\nStarting task: " + cmd); 93 | firstRun = false; 94 | } 95 | 96 | var taskFunc = tasks[cmd]; 97 | if (taskFunc) { 98 | var deferred = taskFunc.apply(this, arguments); 99 | if (deferred) { 100 | var async = this.async(); 101 | deferred.done(async); 102 | } 103 | } else { 104 | grunt.fail.warn("Tizendev task " + cmd + " not found"); 105 | } 106 | }); 107 | }; 108 | -------------------------------------------------------------------------------- /tasks/lib/util.js: -------------------------------------------------------------------------------- 1 | var Q = require("q"); 2 | var fs = require("fs"); 3 | var path = require("path"); 4 | var http = require("http"); 5 | var url = require("url"); 6 | 7 | module.exports = function(grunt) { 8 | "use strict"; 9 | 10 | var _ = grunt.util._; 11 | 12 | var util = { 13 | getModifiedDates: function(dir) { 14 | var files = {}; 15 | grunt.file.recurse(dir, function(path) { 16 | var stat = fs.statSync(path); 17 | files[path] = stat.mtime.getTime(); 18 | }); 19 | return files; 20 | }, 21 | 22 | diffModifiedDates: function(files1, files2) { 23 | var added = []; 24 | var modified = []; 25 | 26 | _.each(files2, function(modified2, path2) { 27 | var modified1 = files1[path2]; 28 | if (modified1) { 29 | if (modified1 != modified2) 30 | modified.push(path2); 31 | } else { 32 | added.push(path2); 33 | } 34 | }); 35 | 36 | return { 37 | added: added, 38 | modified: modified 39 | }; 40 | }, 41 | 42 | isAbsolutePath: function(dirName) { 43 | var trimmed = dirName.trim(); 44 | return trimmed.length > 0 && trimmed.charAt(0) == "/"; 45 | }, 46 | 47 | shellDateFormat: function(date) { 48 | function f(num) { 49 | return num < 10 ? "0" + num : num+""; 50 | } 51 | return f(date.getUTCMonth()+1) + f(date.getUTCDate()) + f(date.getUTCHours()) + f(date.getUTCMinutes()) + date.getUTCFullYear(); 52 | }, 53 | 54 | execUntilTrue: function(deferredFunc) { 55 | var deferred = Q.defer(); 56 | 57 | var exec = function() { 58 | deferredFunc() 59 | .fail(grunt.fail.warn) 60 | .done(function(isTrue) { 61 | if (isTrue) 62 | deferred.resolve(); 63 | else 64 | exec(); 65 | }); 66 | }; 67 | exec(); 68 | 69 | return deferred.promise; 70 | }, 71 | 72 | execTimes: function(deferredFunc, times, sleep) { 73 | var deferred = Q.defer(); 74 | var allResults = []; 75 | (function exec() { 76 | deferredFunc() 77 | .fail(grunt.fail.warn) 78 | .done(function(results) { 79 | allResults.push(results); 80 | if (allResults.length >= times) 81 | deferred.resolve(allResults); 82 | else 83 | setTimeout(exec, sleep); 84 | }); 85 | })(); 86 | return deferred.promise; 87 | }, 88 | 89 | existingPath: function(base, fileName) { 90 | var file = path.join(base, fileName); 91 | if (grunt.file.exists(file)) 92 | return file; 93 | else 94 | return null; 95 | }, 96 | 97 | isBuildDirectory: function(rootPath, buildDir) { 98 | var webAppConfig = this.existingPath(buildDir, "config.xml"); 99 | var hybridAppConfig = this.existingPath(buildDir, "res/wgt/config.xml"); 100 | var configFile = webAppConfig || hybridAppConfig; 101 | 102 | if (configFile) { 103 | var buildAppId = this.getAppId(configFile); 104 | var projectAppId = this.getAppId(path.join(rootPath, "config.xml")); 105 | return buildAppId === projectAppId; 106 | } else { 107 | return false; 108 | } 109 | }, 110 | 111 | removeDsStoreFiles: function(baseDir) { 112 | var dsStoreFiles = grunt.file.expand(path.join(baseDir, "**/.DS_Store")); 113 | dsStoreFiles.forEach(function(filepath) { 114 | grunt.log.warn("Removing " + filepath); 115 | fs.unlink(filepath); 116 | }); 117 | }, 118 | 119 | isEmptyDirectory: function(dir) { 120 | return fs.readdirSync(dir).length === 0; 121 | }, 122 | 123 | getAppId: function(configXmlPath) { 124 | var configXml = grunt.file.read(configXmlPath); 125 | var regexp = new RegExp("]*?id=['\"](.*?)['\"]","i"); 126 | var matches = regexp.exec(configXml); 127 | if (matches.length > 0) { 128 | var appId = matches[1]; 129 | if (appId.indexOf(".") < 0) 130 | grunt.fail.fatal("AppId is in unsupported format: " + appId); 131 | return appId; 132 | } else { 133 | grunt.fail.warn("application id not found in config.xml"); 134 | } 135 | }, 136 | 137 | applyGruntTemplate: function(array) { 138 | return _.map(array, function(str) { 139 | return grunt.template.process(str, { data: grunt.config("tizendev")}); 140 | }); 141 | }, 142 | 143 | resolvedPromise: function() { 144 | return Q.fcall(function() { return true; }); 145 | }, 146 | 147 | triggerLiveReload: function() { 148 | util.httpPost("http://localhost:35729/changed", { 149 | files: "http://localhost:9090/inspector.html" 150 | }); 151 | }, 152 | 153 | parseDlogRow: function(row) { 154 | var regexp = /^([^a-zA-Z]+)/; 155 | var match = regexp.exec(row); 156 | if (match != null) 157 | return { row: row, time: new Date(match[1]) }; 158 | else 159 | return null; 160 | }, 161 | 162 | parseDlogConsoleMessage: function(row) { 163 | var regexp = /^([^a-zA-Z]+).*?:(.*)/; 164 | var match = regexp.exec(row); 165 | if (match != null) 166 | return { row: row, time: new Date(match[1]), message: match[2].trim() }; 167 | else 168 | return null; 169 | }, 170 | 171 | httpPost: function(uri, data) { 172 | var uri = url.parse(uri); 173 | var options = { 174 | hostname: uri.hostname, 175 | port: uri.port, 176 | path: uri.pathname, 177 | method: 'POST' 178 | }; 179 | 180 | var req = http.request(options, function(res) { 181 | res.setEncoding('utf8'); 182 | res.on('data', function (chunk) { 183 | grunt.verbose.writeln('BODY: ' + chunk); 184 | }); 185 | }); 186 | 187 | req.on('error', function(e) { 188 | grunt.verbose.writeln('Problem with request: ' + e.message); 189 | }); 190 | 191 | req.write(JSON.stringify(data)); 192 | req.end(); 193 | } 194 | }; 195 | 196 | return util; 197 | }; 198 | -------------------------------------------------------------------------------- /tasks/lib/shell.js: -------------------------------------------------------------------------------- 1 | var Q = require("q"); 2 | var path = require("path"); 3 | var Bacon = require("baconjs"); 4 | var spawn = require('child_process').spawn; 5 | var fs = require('fs'); 6 | 7 | module.exports = function(grunt) { 8 | "use strict"; 9 | 10 | var _ = grunt.util._; 11 | var util = require("./util.js")(grunt); 12 | 13 | function getBinPath(cmd) { 14 | return path.join(grunt.config("tizendev").binPath, cmd); 15 | } 16 | 17 | var shell = { 18 | exec: function(cmd, args, opts) { 19 | var deferred = Q.defer(); 20 | 21 | this.spawn({ 22 | cmd: cmd, 23 | args: args, 24 | opts: opts 25 | }, function(error, output) { 26 | if (error) 27 | deferred.reject(output); 28 | else 29 | deferred.resolve(output); 30 | }); 31 | return deferred.promise; 32 | }, 33 | 34 | execVerbose: function(cmd, args, opts) { 35 | var optsWithStdout = _.extend({}, { stdio: ['ignore', 1, 2]}, opts); 36 | return this.exec(cmd, args, optsWithStdout); 37 | }, 38 | 39 | startDaemon: function(cmd, args) { 40 | var deferred = Q.defer(); 41 | var proc = spawn(cmd, args, { stdio: ['ignore', 1, 2]}); 42 | proc.on('close', deferred.resolve); 43 | return deferred.promise; 44 | }, 45 | 46 | execSdbShell: function(cmd) { 47 | return this.exec("sdb", ["shell", cmd]) 48 | .fail(grunt.fail.warn); 49 | }, 50 | 51 | execSdb: function(args) { 52 | return this.exec("sdb", args) 53 | .fail(grunt.fail.warn); 54 | }, 55 | 56 | sdbKill: function(appId) { 57 | return this.execSdbShell("wrt-launcher -k " + appId); 58 | }, 59 | 60 | sdbDebug: function(appId) { 61 | return shell.exec( 62 | "sdb", 63 | ["shell", "wrt-launcher", "-d -s", appId] 64 | ); 65 | }, 66 | 67 | sdbDate: function(date) { 68 | if (date == null) { 69 | return shell.execSdbShell("date -R").then(function(output) { 70 | return new Date(output); 71 | }); 72 | } else { 73 | var formattedDate = util.shellDateFormat(date); 74 | return shell.execSdb(["root", "on"]).then(function() { shell.execSdbShell("date -u " + formattedDate); }); 75 | } 76 | }, 77 | 78 | generateMakeFile: function(nativeAppPath) { 79 | return this.execVerbose( 80 | grunt.config("tizendev.binPath") + "native-gen" , 81 | ["makefile", "-t", "app"], 82 | {cwd: nativeAppPath} 83 | ); 84 | }, 85 | 86 | buildNativeProject: function(nativeAppPath) { 87 | 88 | var makeFilePath = path.join(nativeAppPath, "CommandLineBuild"); 89 | console.log(makeFilePath); 90 | if (!fs.existsSync(makeFilePath)) 91 | grunt.fail.warn("Path not found: " + makeFilePath + ". Run generate make file first"); 92 | else { 93 | return this.execVerbose( 94 | grunt.config("tizendev.binPath") + "native-make" , 95 | ["-a", grunt.config("tizendev.nativeTarget")], 96 | {cwd: makeFilePath} 97 | ); 98 | } 99 | }, 100 | 101 | nativeAppToBuildPath: function(nativePath, buildPath, cwd) { 102 | return shell.exec( 103 | getBinPath("web-build"), 104 | [".", "-rp", nativePath, "--output", buildPath], 105 | {cwd: cwd} 106 | ).then(function() { 107 | return shell.removeDirectory(path.join(buildPath, "/res/wgt")); 108 | }); 109 | }, 110 | 111 | sign: function(profileName, profilePath, buildDir) { 112 | return this.execVerbose( 113 | getBinPath("web-signing"), 114 | ["-n", "-p", profileName + ":" + profilePath], 115 | {cwd: buildDir}); 116 | }, 117 | 118 | package: function(wgtName, buildPath) { 119 | var dsStoreFiles = grunt.file.expand(path.join(buildPath, "**/.DS_Store")); 120 | if (dsStoreFiles.length > 0) 121 | grunt.log.warn("Found .DS_Store files. Don't open build directory in Finder as it may change directory contents after signing. " + 122 | "(" + dsStoreFiles.join() + ")"); 123 | 124 | return this.exec( 125 | getBinPath("webtizen"), 126 | ["-p", "-n", wgtName, buildPath]).then(grunt.log.writeln); 127 | }, 128 | 129 | isWidgetStopped: function(appId) { 130 | return shell.exec( 131 | "sdb", 132 | ["shell", "wrt-launcher", "-r", appId] 133 | ) 134 | .fail(grunt.fail.warn) 135 | .then(function(output) { 136 | return output.indexOf("result: running") < 0; 137 | }); 138 | }, 139 | 140 | installWidget: function(wgtName) { 141 | function install() { 142 | return shell.execVerbose(getBinPath("webtizen"), ["-i", "-w", wgtName]); 143 | } 144 | 145 | function isCorrectDate(targetDate) { 146 | return Math.abs(new Date().getTime() - targetDate.getTime()) < 1000*60*5; 147 | } 148 | 149 | function resetDateAndInstall() { 150 | return shell.sdbDate().then(function(targetDate) { 151 | if (!isCorrectDate(targetDate)) { 152 | grunt.log.warn("Target device's clock is not set correctly. Syncing target device's clock with host and trying again..."); 153 | return shell.sdbDate(new Date()).then(install); 154 | } 155 | }); 156 | } 157 | 158 | return install().fail(function(output) { 159 | grunt.log.error(output); 160 | return resetDateAndInstall().fail(function() { 161 | throw new Error("Installation failed."); 162 | }); 163 | }); 164 | }, 165 | 166 | uninstallWidget: function(appId) { 167 | return shell.exec( 168 | "sdb", 169 | ["shell", "wrt-installer", "-un", appId] 170 | ); 171 | }, 172 | 173 | spawn: function(args, done) { 174 | return grunt.util.spawn(args, function(error, result, code) { 175 | done(error, result.stdout + (result.stderr.length > 0 ? "\n" + result.stderr : "")); 176 | }); 177 | }, 178 | 179 | readChildProcessOutput: function(childProcess) { 180 | var rl = require('readline'); 181 | var reader = rl.createInterface(childProcess.stdout, childProcess.stdin); 182 | var bus = new Bacon.Bus(); 183 | reader.on("line", function(data) { bus.push(data); }); 184 | return bus; 185 | }, 186 | 187 | removeDirectory: function(dir) { 188 | return shell.exec("rm", ["-rf", dir]); 189 | }, 190 | 191 | clearDlog: function() { 192 | return shell.exec( 193 | "sdb", 194 | ["dlog", "-c"] 195 | ); 196 | }, 197 | 198 | getDlogProcess: function() { 199 | return shell.spawn({ 200 | cmd: "sdb", 201 | args: ["dlog", "-v", "time", "EFL", "ConsoleMessage", "WRT", "WEBKIT", "AUL"] 202 | }, function(error, output) { 203 | }); 204 | }, 205 | 206 | getPlatformLevelLogging: function() { 207 | return shell.exec( 208 | "sdb", 209 | ["shell", "dlogctrl", "get", "platformlog"] 210 | ); 211 | }, 212 | 213 | setPlatformLevelLogging: function(enable) { 214 | return shell.exec( 215 | "sdb", 216 | ["shell", "dlogctrl", "set", "platformlog", enable ? "1" : "0"] 217 | ); 218 | }, 219 | 220 | enablePlatformLevelLoggingIfDisabled: function() { 221 | var that = this; 222 | var deferred = Q.defer(); 223 | this.getPlatformLevelLogging() 224 | .then(function(result) { 225 | var enabled = result == "1" 226 | if(enabled) { 227 | deferred.resolve(); 228 | } 229 | else { 230 | that.setPlatformLevelLogging(true) 231 | .then(function(){ 232 | deferred.reject("Enabled platform level logging for profiling, please restart the target device"); 233 | }); 234 | } 235 | 236 | }); 237 | return deferred.promise; 238 | } 239 | }; 240 | 241 | return shell; 242 | }; -------------------------------------------------------------------------------- /fps-measurement/fpstest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ $# != 1 ]; then 6 | echo "Usage: $(basename $0) TEST_CASE_FILE|TEST_CASE_DIR" 7 | echo " if directory given, run the .fpstest file inside it" 8 | exit 1 9 | fi 10 | 11 | die() { 12 | echo "ERROR: $@" 13 | exit 1 14 | } 15 | 16 | if [ -d "$1" ]; then 17 | if [ $(ls $1/*.fpstest | wc -l) = 1 ]; then 18 | TEST_CASE_FILE=$(ls $1/*.fpstest) 19 | else 20 | die "Found no .fpstest file or more than one .fpstest file from $1" 21 | fi 22 | else 23 | TEST_CASE_FILE="$1" 24 | fi 25 | 26 | if [ -z $(which sdb) ]; then 27 | die "sdb not found in PATH. Install Tizen SDK and make sure that the sdb tool is in PATH." 28 | fi 29 | 30 | if [ "$(sdb devices | wc -l)" -lt 2 ]; then 31 | die "No phone or emulator found" 32 | fi 33 | 34 | canonicalize_path() { 35 | ARG_PATH="$(dirname $1)/$(basename $1)" 36 | while [ -L "$ARG_PATH" ]; do 37 | ARG_PATH=$(readlink "$ARG_PATH") 38 | done 39 | echo "$ARG_PATH" 40 | } 41 | 42 | run_cmd_on_target() { 43 | sdb shell $@ | tr -d '\r' # sdb adds '\r' characters - strip them away 44 | } 45 | 46 | SCRIPT_PATH=$(canonicalize_path "$0") 47 | FPSLOG_FILE=/tmp/fpslog 48 | 49 | TARGET_UNAME_M=$(run_cmd_on_target uname -m) 50 | if [ "$TARGET_UNAME_M" = "armv7l" ]; then 51 | TARGET_ARCH="armv7l" 52 | elif [ "$TARGET_UNAME_M" = "i686_emulated" ]; then 53 | TARGET_ARCH="i586" 54 | else 55 | die "Unknown target architecture: $TARGET_UNAME_M" 56 | fi 57 | 58 | set_root_mode() { 59 | if [ ! $(run_cmd_on_target "whoami") = "root" ]; then 60 | echo "target not in root mode, setting..." 61 | sdb root on 62 | fi 63 | } 64 | 65 | reboot_or_die() { 66 | REBOOT_PROG="restart" 67 | if [ -z $(run_cmd_on_target "which $REBOOT_PROG") ]; then 68 | die "Cannot reboot target programmatically, please reboot manually. Quitting..." 69 | fi 70 | echo -n "Reboot now? [Y/n] " 71 | read answer 72 | if [ -z "$answer" ] || [ "$answer" = y ] || [ "$answer" = Y ]; then 73 | run_cmd_on_target $REBOOT_PROG 74 | exit 0 75 | else 76 | echo "Not rebooting. Cannot continue before reboot, quitting..." 77 | exit 0 78 | fi 79 | } 80 | 81 | enable_update_mode_on_target() { 82 | run_cmd_on_target "which change-booting-mode.sh &> /dev/null && change-booting-mode.sh --update" 83 | } 84 | 85 | check_if_fps_logging_enabled_and_try_to_enable() { 86 | FPS_DEBUG_ENVIRONMENT_VARIABLE="ECORE_EVAS_FPS_DEBUG=1" 87 | FPS_DEBUG_ENVIRONMENT_FILE="/etc/profile.d/fps-debug.sh" 88 | 89 | WRT_LAUNCHPAD_PID=$(run_cmd_on_target "pidof wrt_launchpad_daemon | head -n 1") 90 | [ -n "$WRT_LAUNCHPAD_PID" ] || die "wrt_launchpad_daemon not running on target" 91 | 92 | WRT_LAUNCHPAD_ENVIRONMENT=$(run_cmd_on_target "cat /proc/$WRT_LAUNCHPAD_PID/environ" | tr '\0' '\n') 93 | if echo "$WRT_LAUNCHPAD_ENVIRONMENT" | grep -q "$FPS_DEBUG_ENVIRONMENT_VARIABLE"; then 94 | return 0 # FPS debugging is enabled 95 | else 96 | echo "FPS debugging is not enabled, enabling for future reboots..." 97 | enable_update_mode_on_target 98 | RES=$(run_cmd_on_target "echo 'export $FPS_DEBUG_ENVIRONMENT_VARIABLE' > '$FPS_DEBUG_ENVIRONMENT_FILE' && chmod 755 '$FPS_DEBUG_ENVIRONMENT_FILE' && echo success") 99 | [ "$RES" = success ] || die "Unable to enable FPS debugging: $RES" 100 | echo "The target must be rebooted to enable FPS debugging. Run this script again after reboot." 101 | reboot_or_die 102 | fi 103 | } 104 | 105 | check_if_package_installed_and_try_to_install() { 106 | BINARY_TO_PROBE="$1" 107 | PACKAGE_NAME_VERSION="$2" 108 | 109 | if [ -z $(run_cmd_on_target "which $BINARY_TO_PROBE") ]; then 110 | RPM_PACKAGE="$PACKAGE_NAME_VERSION.$TARGET_ARCH.rpm" 111 | show_package_installation_instructions_and_die() { 112 | echo " 1) sdb root on; sdb shell 'change-booting-mode.sh --update'" 113 | echo " 2) sdb push $RPM_PACKAGE /tmp" 114 | echo " 3) sdb shell 'rpm -Uvh /tmp/$RPM_PACKAGE'" 115 | exit 1 116 | } 117 | 118 | RPM_PACKAGE_PATH="$(dirname $SCRIPT_PATH)/rpms/$RPM_PACKAGE" 119 | if [ -r "$RPM_PACKAGE_PATH" ]; then 120 | echo "$BINARY_TO_PROBE not found in target, trying to install..." 121 | sdb push "$RPM_PACKAGE_PATH" /tmp 122 | enable_update_mode_on_target 123 | run_cmd_on_target "rpm -Uvh /tmp/$RPM_PACKAGE" 124 | if [ -n $(run_cmd_on_target "which $BINARY_TO_PROBE") ]; then 125 | echo "$PACKAGE_NAME_VERSION installation succesful" 126 | else 127 | echo "$PACKAGE_NAME_VERSION installation failed, install manually:" 128 | show_package_installation_instructions_and_die 129 | fi 130 | else 131 | echo "$PACKAGE_NAME_VERSION not found in target or $RPM_PACKAGE_PATH, install manually:" 132 | show_package_installation_instructions_and_die 133 | fi 134 | fi 135 | } 136 | 137 | read_test_case_file() { 138 | if ! [ -r "$TEST_CASE_FILE" ]; then 139 | die "Cannot read $TEST_CASE_FILE" 140 | fi 141 | echo "Reading test case from $TEST_CASE_FILE..." 142 | . "$(dirname $TEST_CASE_FILE)/$(basename $TEST_CASE_FILE)" 143 | [ -z "$APPID" ] && die "APPID not specified in $TEST_CASE_FILE" 144 | [ $(type -t testcase) != "function" ] \ 145 | && die "testcase function not specified in $TEST_CASE_FILE" 146 | echo "Test case file read" 147 | } 148 | 149 | 150 | run_xmacro() { 151 | MACRO_FILE="$1" 152 | 153 | echo -n "Running macro ${MACRO_FILE}..." 154 | PATH_TO_MACRO_FILE="$(dirname $TEST_CASE_FILE)/$MACRO_FILE" 155 | sdb push "$PATH_TO_MACRO_FILE" /tmp &> /dev/null 156 | run_cmd_on_target "xmacroplay :0 < '/tmp/$(basename $MACRO_FILE)' &> /dev/null" 157 | run_cmd_on_target "rm '/tmp/$(basename $MACRO_FILE)'" 158 | echo "done" 159 | } 160 | start_fps_log() { 161 | echo "Starting FPS measurement" 162 | run_cmd_on_target "true > '$FPSLOG_FILE'" # truncate 163 | } 164 | 165 | kill_application() { 166 | echo "Killing app..." 167 | run_cmd_on_target "wrt-launcher --kill '$APPID' 2>&1 > /dev/null" 168 | sleep 0.5 169 | killall "$APPID" &> /dev/null || true 170 | sleep 1.5 # killing is asynchronous, wait a little... 171 | } 172 | 173 | launch_application_and_grab_stdout() { 174 | echo "Launching app..." 175 | run_cmd_on_target "wrt-launcher --start $APPID" 176 | redirect_fps_output_to_file 177 | } 178 | redirect_fps_output_to_file() { 179 | rm -f $FPSLOG_FILE 180 | 181 | GDB_SCRIPT_PATH="/tmp/redirect_stdout_to_file.gdb" 182 | create_gdb_script "$GDB_SCRIPT_PATH" 183 | 184 | RETRIES=10 185 | for retry in $(eval echo {1..$RETRIES}); do 186 | APP_PID=$(run_cmd_on_target "pidof $APPID" | head -n 1) 187 | if [ -n "$APP_PID" ]; then 188 | break 189 | fi 190 | if [ $retry == $RETRIES ]; then 191 | die "could not attach to process" 192 | else 193 | echo "app not found, retrying ($retry/$RETRIES)..." 194 | fi 195 | sleep 0.5 196 | done 197 | run_cmd_on_target "gdb -p $APP_PID -x '$GDB_SCRIPT_PATH'" > /dev/null 198 | } 199 | create_gdb_script() { 200 | sdb shell "cat > $GDB_SCRIPT_PATH <&1 > /dev/null 210 | run_cmd_on_target "grep -a 'FPS:' '$FPSLOG_FILE'" > "$FPSLOG_FILE" 211 | [ -s "$FPSLOG_FILE" ] || die "Could not retrieve FPS log from target or testcase too short" 212 | 213 | # Remove two first and two last lines from log - they're inaccurate 214 | FPSLOG_LINE_COUNT=$(wc -l < "$FPSLOG_FILE") 215 | sed -n "2,$(( $FPSLOG_LINE_COUNT - 2 ))p" < "${FPSLOG_FILE}" > "${FPSLOG_FILE}.2" 216 | mv "${FPSLOG_FILE}.2" "$FPSLOG_FILE" 217 | awk ' 218 | BEGIN { FS = "[ ,]+" } 219 | /^FRAME: / { sum += data[NR] = $4 } 220 | END { 221 | avg = sum / NR 222 | for (i in data) { 223 | diff = data[i] - avg 224 | sqsum += diff * diff 225 | } 226 | stddev = sqrt(sqsum / (NR - 1)) 227 | 228 | printf "FPS: %.1f ± %.2f (N=%d)\n", avg, stddev, NR 229 | }' "$FPSLOG_FILE" 230 | } 231 | 232 | run_testcase() { 233 | echo "Running testcase..." 234 | testcase 235 | } 236 | 237 | set_root_mode 238 | check_if_fps_logging_enabled_and_try_to_enable 239 | check_if_package_installed_and_try_to_install xmacroplay xmacro-pre0.3_reaktor-1 240 | check_if_package_installed_and_try_to_install gdb gdb-7.2-2.1 241 | read_test_case_file 242 | 243 | kill_application 244 | 245 | launch_application_and_grab_stdout 246 | run_testcase 247 | show_fps_stats 248 | 249 | kill_application 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tizendev 2 | 3 | > Tizendev - Develop Tizen web apps from command line 4 | 5 | Tizendev consists of two separate and independent tool collections: 6 | 1. tizendev 7 | * Build, deploy and run Tizen Web Apps 8 | * Profile Tizen Web Apps 9 | * Watch changes 10 | * Debug 11 | 2. FPS Measurement Tools (fps-measurement/) 12 | * Measure FPS of Tizen Web App 13 | 14 | ## Requirements 15 | 16 | Before you try to use Tizendev plugin, make sure to install the latest version of `nodeJS`. Then install Grunt command-line helper (`npm install -g grunt-cli`). 17 | 18 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. 19 | 20 | ## Getting Started 21 | Tizendev makes it easier to build, run, install and debug Tizen Web Apps from command line. 22 | 23 | **Note that hybrid or native applications are not currently supported.** 24 | 25 | There are two ways to use the plugin: 26 | 27 | * *Global install*. The easier choice. No project-specific configuration is made. The plugin is launched using `tizendev` script. Configuration settings can be overridden as command-line arguments to `tizendev`. Additional tasks, such as minifying, can't be specified. 28 | 29 | * *Local install*. The plugin is installed as a Grunt plugin to the project. Project-specific settings, such as complex Grunt tasks, can be specified. 30 | 31 | 32 | 33 | ### "Global" install 34 | 35 | If you don't want to install the plugin for every application you develop, you can install it globally to be able to use most of the functionality. 36 | 37 | Clone the repository, and run `npm install` in that directory. 38 | 39 | Then edit the default settings in Gruntfile.js. At least, configure signing options: the name of profile specified in profiles.xml and optionally the name of profile in profiles.xml. The settings should be the same you have configured in Eclipse. 40 | 41 | ```js 42 | tizendev: { 43 | profile: "nameOfProfile", // if left empty, this is parsed from profiles.xml 44 | profilePath: "~/workspace/.metadata/.plugins/org.tizen.common.sign/profiles.xml", 45 | tasks: { 46 | } 47 | } 48 | ``` 49 | 50 | Remember to add the cloned repository to system path so that `tizendev` shell script can be run from any project folder. 51 | 52 | To build and run Tizen Web Apps, go to a project directory (directory containing config.xml) and execute the `tizendev` shell script. The script will launch Grunt and configure the plugin to use the current working directory. 53 | 54 | ``` 55 | cd ~/projectDirectory 56 | tizendev taskName 57 | ``` 58 | 59 | For valid task names, see `Tasks`. 60 | 61 | Settings in Gruntfile.js apply to all projects. You can override any setting that is a string, number or array by specifying it as an argument: 62 | 63 | ``` 64 | tizendev profile --profilingTimes=5 65 | ``` 66 | For configuration options, see `Configuring the "tizendev" task`. 67 | 68 | 69 | ### Local install 70 | 71 | By installing Tizendev locally, you can use it as a Grunt plugin, and unleash the full potential of Grunt tasks and plugins. For example, you can minify JS files on fly. 72 | 73 | First, clone Tizendev to a folder. Then, go to your web app project folder and install the plugin using npm: 74 | 75 | `npm install path-to-tizendev/` 76 | 77 | Also, install Grunt locally if it hasn't been installed already: 78 | 79 | `npm install grunt@0.4.1 --save-dev` 80 | 81 | Then install dependencies: 82 | 83 | `npm install grunt-contrib-copy --save-dev` 84 | 85 | `npm install grunt-contrib-watch --save-dev` 86 | 87 | `npm install grunt-tizen --save-dev` 88 | 89 | Once the plugin has been installed, it may be enabled inside your Gruntfile with these lines of JavaScript: 90 | 91 | ```js 92 | grunt.loadNpmTasks("grunt-contrib-watch"); 93 | grunt.loadNpmTasks("grunt-contrib-copy"); 94 | grunt.loadNpmTasks("grunt-tizen"); 95 | grunt.loadNpmTasks('tizendev'); 96 | ``` 97 | 98 | The plugin is ran like any other Grunt plugin: 99 | ``` 100 | grunt tizendev:taskname [optional arguments] 101 | ``` 102 | 103 | for example: 104 | 105 | ``` 106 | grunt tizendev:profile --profilingTimes=5 107 | ``` 108 | 109 | ## Configuring the "tizendev" task 110 | 111 | ### Overview 112 | 113 | These settings are set in Gruntfile.js. If you use a global install, the file is located in the same folder as `tizendev` script file. If you use local install, the file is located in your web app folder. 114 | 115 | In your Gruntfile, add a section named `tizendev` to the data object passed into `grunt.initConfig()`. 116 | 117 | ```js 118 | grunt.initConfig({ 119 | tizendev: { 120 | // Config goes here. 121 | }, 122 | }) 123 | ``` 124 | ### Options 125 | 126 | These default options can be set in `Gruntfile.js`. Any string, number or array setting can also be overriden by specifying it as an argument to Tizendev script or Grunt. 127 | 128 | ```js 129 | tizenDev: { 130 | watch: ["**", "!node_modules/**"], // Files that trigger tizendev tasks. Should include all files that are monitored by copy and task filters. Build path is excluded automatically. 131 | watchExclude: [], // This is appended to watch - useful for excluding files in project's gruntfile 132 | copy: ["**", "!node_modules/**", "!*.wgt"], // What files to copy to build folder (and sync to phone). 133 | copyExclude: [], // This is appended to copy - useful for excluding files in project's gruntfile 134 | tasks: {}, // tasks to run after file change. for example: tasks: { "uglify": ["js/*.js", "otherfolder/**/*.js"] } 135 | binPath: "~/tizen-sdk/tools/ide/bin/", // location of CLI tools 136 | profile: "", // the name of the profile used for signing 137 | nativeTarget: "armel", //specify an architecture ("armel" | "i386") 138 | profile: "", // the name of the profile used for signing. If empty, profile name is parsed from profiles.xml 139 | profilePath: "", // location of profiles.xml 140 | sourceDir: ".", // root folder, ie. folder where config.xml is located 141 | buildPath: "tizendevbuild", // build folder, must NOT be project root. 142 | fileChanged: undefined, // custom function to execute after file change. Receives file name as argument. 143 | remoteAppLocation: "/opt/usr/apps/<%=tizendev.appId%>/res/wgt/", // Application directory on tizen device. appId is determined runtime. 144 | profilingEvents: ["launch request : <%=tizendev.fullAppId%>", "getUri(): default uri", "E_PARSED", "E_LOADED"], // Log events that trigger profiling 145 | profilingTimes: 1, // number of profiling attempts 146 | profilingSleep: 5, // seconds between profiling rounds 147 | liveReload: true // enable livereload server when in debug mode 148 | }; 149 | ``` 150 | 151 | ### Sample Gruntfile.js 152 | 153 | This example applies only when using *local* install. 154 | 155 | ```js 156 | module.exports = function(grunt) { 157 | grunt.initConfig({ 158 | tizendev: { 159 | // Set signing options 160 | profilePath: "~/workspace/.metadata/.plugins/org.tizen.common.sign/profiles.xml", 161 | 162 | // profile name can be specified but usually it is not necessary 163 | // profile: "profile", 164 | 165 | // Trigger uglify task on build and when any file matching js/*.js changes. 166 | tasks: { 167 | "uglify": "js/*.js" 168 | }, 169 | 170 | // Exclude all js files under js/ folder from build. They aren't needed, because the source code is minified and copied to build directory in uglify task. 171 | copyExclude: ["!js/**/*.js"] 172 | }, 173 | 174 | // Configure grunt-contrib-uglify plugin to minimize all files under js/ folder and save the result in build directory 175 | uglify: { 176 | target: { 177 | files: { 178 | "tizendevbuild/minimized.js": "js/*.js" 179 | } 180 | } 181 | } 182 | }); 183 | 184 | // Load dependencies 185 | grunt.loadNpmTasks("grunt-contrib-copy"); 186 | grunt.loadNpmTasks("grunt-contrib-watch"); 187 | grunt.loadNpmTasks("grunt-tizen"); 188 | // Load the plugin 189 | grunt.loadNpmTasks("tizendev"); 190 | // Load uglify plugin 191 | grunt.loadNpmTasks("grunt-contrib-uglify"); 192 | }; 193 | 194 | ``` 195 | 196 | ### Tasks 197 | 198 | #### tizendev:develop 199 | 200 | - run all tasks needed to start developing an app: 201 | * `connect` 202 | * `targetImage' 203 | * `build` 204 | * `sign` 205 | * `package` 206 | * `uninstall` (this helps to avoid some bugs in installation process) 207 | * `install` 208 | * `restart` 209 | * `watch` 210 | 211 | #### tizendev:connect 212 | 213 | - poll `sdb devices` until a target device is connected 214 | 215 | #### tizendev:targetImage 216 | 217 | - show target device image version 218 | 219 | #### tizendev:clean 220 | 221 | - remove the build directory 222 | 223 | #### tizendev:build 224 | 225 | - run `clean` 226 | 227 | - copy files using `copy` filter to the build folder 228 | 229 | - then execute tasks mentioned in `tasks` 230 | 231 | #### tizendev:start, stop, restart 232 | 233 | - start/stop/restart application 234 | 235 | #### tizendev:sign 236 | 237 | - sign files in build folder (run `build` prior to `sign`) 238 | 239 | - `profile` and `profilePath` options must be specified 240 | 241 | - tizen CLI will not throw error if signing fails! 242 | 243 | #### tizendev:package 244 | 245 | - create widget from all files in build folder 246 | 247 | - widget file is saved to current working directory 248 | 249 | - app in build folder must be signed before calling package 250 | 251 | - previous widget file is removed 252 | 253 | #### tizendev:install 254 | 255 | - install widget file to the target device 256 | 257 | - uses the widget file created by `package` command 258 | 259 | #### tizendev:uninstall 260 | 261 | - uninstall widget from target device 262 | 263 | #### tizendev:watch 264 | 265 | - watch for changes in files defined by `files` option 266 | 267 | - if changed file matches `copy` filter, the file is copied 268 | 269 | - if changed file matches a filter in `tasks`, the task(s) is executed 270 | 271 | - finally, all changed files in build directory are transferred to phone and the application is restarted 272 | 273 | #### tizendev:setDate 274 | 275 | - set current date/time to target device 276 | 277 | #### tizendev:profile 278 | 279 | - launch profiler 280 | 281 | ### Debugging 282 | 283 | Start the script with `--debug` argument. For example: 284 | 285 | ``` 286 | tizendev develop --debug 287 | ``` 288 | 289 | Then go to http://localhost:9090/inspector.html?page=1 290 | 291 | If you have the LiveReload browser plugin (http://livereload.com) installed, the inspector will be automatically refreshed when files are updated on target device. 292 | 293 | 294 | ### Profiling 295 | 296 | The script can be used to measure start up times of your application. By running `profile` task, the script will launch the application and start monitor console log. When all required console messages have been received, the script displays the time it took to receive the messages and terminates the application. 297 | 298 | By default, the profile script waits for console messages `E_PARSED` and `E_LOADED`. So, in appropriate locations, add 299 | 300 | ```js 301 | console.log("E_PARSED"); // script has been parsed 302 | ``` 303 | 304 | and 305 | 306 | ```js 307 | console.log("E_LOADED"); // app is ready to use 308 | ``` 309 | 310 | Then run `profile` and optionally specify how many times you want to repeat the profiling task. 311 | 312 | ``` 313 | tizendev profile --profilingTimes=5 314 | ``` 315 | ### Using profiling with emulator 316 | You have to run `dlogctrl set platformlog 1` in the emulator's shell to enable enough logging for the profiler. 317 | After running the command restart the emulator. 318 | 319 | 320 | ### Troubleshooting 321 | Installation may fail for many reasons. The error message will not tell the actual reason. Possible solutions: 322 | * Have you accidentally included unnecessary signature files in source files? It seems to cause installation to fail. Remove all files except of necessary project files. 323 | * Installation also fails if the target device's clock is in wrong time. 324 | * OS X may add .DS_Store file to build directory after signing. This will make installation fail because package contents don't match the signature. Get rid of .DS_Store files in build path. 325 | * If you try to compile the project in Eclipse, installation may fail if Tizendev build folder's signature files are included in the Eclipse project. The easiest solution is to remove the build folder. You can also set build path to point at some location outside the Eclipse project. 326 | 327 | ## Release History 328 | _(Nothing yet)_ 329 | 330 | ## License 331 | ``` 332 | The MIT License (MIT) 333 | 334 | Copyright (c) 2013 Reaktor Innovations Oy 335 | 336 | Permission is hereby granted, free of charge, to any person obtaining a copy of 337 | this software and associated documentation files (the "Software"), to deal in 338 | the Software without restriction, including without limitation the rights to 339 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 340 | the Software, and to permit persons to whom the Software is furnished to do so, 341 | subject to the following conditions: 342 | 343 | The above copyright notice and this permission notice shall be included in all 344 | copies or substantial portions of the Software. 345 | 346 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 347 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 348 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 349 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 350 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 351 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 352 | ``` 353 | -------------------------------------------------------------------------------- /tasks/lib/tasks.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | var fs = require("fs"); 3 | var Q = require("q"); 4 | var Bacon = require("baconjs"); 5 | 6 | module.exports = function(grunt) { 7 | "use strict"; 8 | 9 | var util = require("./util.js")(grunt); 10 | var shell = require("./shell.js")(grunt); 11 | var profiler = require("./profiling.js")(grunt); 12 | var _ = grunt.util._; 13 | var gruntTizenInitialized = false; 14 | 15 | var tasks = { 16 | clean: function() { 17 | if (util.isBuildDirectory(getProjectFilePath(""), getConfig().buildPath) || 18 | (grunt.file.exists(getConfig().buildPath) && util.isEmptyDirectory(getConfig().buildPath))) { 19 | grunt.log.writeln("Removing " + getConfig().buildPath); 20 | return shell.removeDirectory(getConfig().buildPath); 21 | } else if (grunt.file.exists(getConfig().buildPath)) { 22 | grunt.fail.fatal("Path " + getConfig().buildPath + " does not contain current project " + getConfig().fullAppId); 23 | } else 24 | return util.resolvedPromise(); 25 | }, 26 | 27 | targetImage: function() { 28 | return shell.execSdbShell("cat /etc/zypp/repos.d/slp-release.repo").then(function(output) { 29 | var regexp = /.*?(20\d\d)(\d\d)(\d\d)\D/; 30 | var groups = regexp.exec(output); 31 | if (groups) { 32 | grunt.log.writeln("Target image updated: " + new Date(groups[1], groups[2], groups[3]).toDateString()); 33 | } else { 34 | grunt.log.writeln("Target image: " + output); 35 | } 36 | }); 37 | }, 38 | 39 | build: function() { 40 | return tasks.clean() 41 | .then(buildNativeAppIfNecessary) 42 | .then(buildWidget); 43 | }, 44 | 45 | start: function() { 46 | var action = grunt.option("debug") ? "debug" : "start"; 47 | 48 | grunt.config.set("tizen." + action, { 49 | action: action, 50 | localPort: 9090, 51 | stopOnFailure: true 52 | }); 53 | gruntTizenTask(action); 54 | }, 55 | 56 | stop: function() { 57 | function waitForStop() { 58 | return util.execUntilTrue(function() { 59 | return shell.isWidgetStopped(getConfig().fullAppId); 60 | }); 61 | } 62 | return shell.sdbKill(getConfig().fullAppId).then(waitForStop); 63 | }, 64 | 65 | restart: function() { 66 | return tasks.stop().then(tasks.start); 67 | }, 68 | 69 | sign: function() { 70 | var config = getConfig(); 71 | util.removeDsStoreFiles(getConfig().buildPath); 72 | 73 | if (!config.profilePath || config.profilePath.length === 0) 74 | grunt.fail.warn("profilePath must be specified."); 75 | 76 | return getSigningProfileName(config.profilePath).then(function(profileName) { 77 | grunt.log.writeln("Signing " + config.buildPath + " using profile '" + profileName + "'"); 78 | return shell.sign(profileName, getConfig().profilePath, getConfig().buildPath); 79 | }); 80 | }, 81 | 82 | package: function() { 83 | removeWgtFile(); 84 | return shell.package(getWgtName(), getConfig().buildPath); 85 | }, 86 | 87 | install: function() { 88 | return shell.installWidget(getWgtName()); 89 | }, 90 | 91 | uninstall: function() { 92 | return shell.uninstallWidget(getConfig().fullAppId).then(grunt.log.writeln); 93 | }, 94 | 95 | watch: function() { 96 | var config = getConfig(); 97 | var srcPaths = _.flatten([ 98 | config.watch, 99 | excludeBuildFolderFilter(config), 100 | config.watchExclude 101 | ]); 102 | 103 | grunt.config.set("watch.tizendev.files", srcPaths); 104 | grunt.config.set("watch.tizendev.options.cwd", config.sourceDir); 105 | grunt.config.set("watch.tizendev.options.spawn", false); 106 | 107 | if (config.liveReload && grunt.option("debug")) { 108 | grunt.config.set("watch.livereload.files", [getProjectFilePath("index.html")]); 109 | grunt.config.set("watch.livereload.options.livereload", true); 110 | } 111 | 112 | grunt.task.run("tizendev:restart"); 113 | grunt.task.run("watch"); 114 | }, 115 | 116 | develop: function() { 117 | grunt.task.run("tizendev:connect"); 118 | grunt.task.run("tizendev:targetImage"); 119 | grunt.task.run("tizendev:build"); 120 | grunt.task.run("tizendev:sign"); 121 | grunt.task.run("tizendev:package"); 122 | grunt.task.run("tizendev:uninstall"); 123 | grunt.task.run("tizendev:install"); 124 | grunt.task.run("tizendev:watch"); 125 | }, 126 | 127 | connect: function() { 128 | return startSdbServer().then(searchForDevice); 129 | }, 130 | 131 | refreshBrowser: function() { 132 | if (grunt.option("debug")) 133 | util.triggerLiveReload(); 134 | }, 135 | 136 | profile: function() { 137 | return shell.enablePlatformLevelLoggingIfDisabled() 138 | 139 | .then(function(){ 140 | return util.execTimes(tasks.profileOnce, getConfig().profilingTimes, getConfig().profilingSleep * 1000); 141 | }) 142 | .then(profiler.printProfilingResult); 143 | }, 144 | 145 | profileOnce: function() { 146 | return shell.clearDlog() 147 | .then(function() { 148 | tasks.stop().done(function() { 149 | shell.sdbDebug(getConfig().fullAppId); 150 | }); 151 | return profiler.waitDLogEvents(); 152 | }) 153 | .then(profiler.printProfilingRow); 154 | }, 155 | 156 | setDate: function() { 157 | return shell.sdbDate(new Date()); 158 | }, 159 | 160 | console: function() { 161 | shell.clearDlog().done(function() { 162 | var child = shell.getDlogProcess(); 163 | var startEvent = profiler.getStartEventName(); 164 | 165 | function isConsoleMessage(line) { return line.indexOf("ConsoleMessage") >= 0; }; 166 | function isStartEvent(line) { return line.indexOf(startEvent) >= 0; }; 167 | function yellow(text) { return '\u001b[33m' + text + '\u001b[0m'; } 168 | 169 | function printRowItem(item) { 170 | var prev = item.prev; 171 | var current = item.current; 172 | var diffFromPrev = (prev != null) ? " (Δ "+ (current.time.getTime() - prev.time.getTime()) + " ms)" : ""; 173 | var timeDiff = item.start ? current.time.getTime() - item.start.time.getTime() : "?"; 174 | console.log(timeDiff + " ms" + diffFromPrev + ": " + yellow(current.message)); 175 | } 176 | 177 | function printStartTime() { 178 | console.log("\n0ms: " + yellow("Application launched")); 179 | } 180 | 181 | var events = shell.readChildProcessOutput(child).scan({}, function(data, current) { 182 | if (isStartEvent(current)) { 183 | var item = util.parseDlogRow(current); 184 | return { 185 | start: item, 186 | current: item 187 | }; 188 | } else if (isConsoleMessage(current)) 189 | return { 190 | start: data.start, 191 | prev: data.current, 192 | current: util.parseDlogConsoleMessage(current) 193 | } 194 | else 195 | return data; 196 | }).skipDuplicates().changes(); 197 | 198 | events.filter(function(item) { return item.prev == null; }).onValue(printStartTime); 199 | events.filter(function(item) { return item.prev != null; }).onValue(printRowItem); 200 | }); 201 | 202 | return Q.defer().promise; 203 | } 204 | }; 205 | 206 | function buildNativeAppIfNecessary() { 207 | var nativeApp = getConfig().nativePath; 208 | 209 | if (nativeApp.length > 0) { 210 | if (!util.isAbsolutePath(nativeApp)) 211 | nativeApp = getProjectFilePath(nativeApp); 212 | 213 | grunt.log.writeln("Linking native application: " + nativeApp); 214 | return buildNative().then(function() { 215 | return shell.nativeAppToBuildPath(nativeApp, getConfig().buildPath, getProjectFilePath("")); 216 | }); 217 | } else 218 | return util.resolvedPromise(); 219 | } 220 | 221 | function getNativeAppPath() { 222 | var nativeApp = getConfig().nativePath; 223 | 224 | if (nativeApp.length > 0) { 225 | if (!util.isAbsolutePath(nativeApp)) 226 | return getProjectFilePath(nativeApp); 227 | else 228 | return nativeApp; 229 | } else { 230 | return null; 231 | } 232 | } 233 | 234 | function buildNative() { 235 | var nativeAppPath = getNativeAppPath(); 236 | 237 | if (nativeAppPath) { 238 | 239 | if (!fs.existsSync(nativeAppPath)) 240 | grunt.fail.warn("Path not found: " + nativeAppPath); 241 | else { 242 | return shell.generateMakeFile(nativeAppPath) 243 | .then(function(){ 244 | return shell.buildNativeProject(nativeAppPath); 245 | }); 246 | } 247 | } 248 | } 249 | 250 | function buildWidget() { 251 | var config = getConfig(); 252 | var srcPaths = _.flatten([ 253 | config.copy, 254 | excludeBuildFolderFilter(config), 255 | config.copyExclude 256 | ]); 257 | 258 | var widgetDirectory = getConfig().nativePath.length > 0 ? path.join(getConfig().buildPath, "/res/wgt") : getConfig().buildPath; 259 | console.log("Building widget to directory: " + widgetDirectory); 260 | 261 | grunt.config.set("copy.tizendev.expand", true); 262 | grunt.config.set("copy.tizendev.cwd", getProjectFilePath("")); 263 | grunt.config.set("copy.tizendev.src", srcPaths); 264 | grunt.config.set("copy.tizendev.dest", widgetDirectory); 265 | grunt.task.run("copy:tizendev"); 266 | executeAllTasks(); 267 | } 268 | 269 | function startSdbServer() { 270 | return shell.startDaemon("sdb", ["start-server"]); 271 | } 272 | 273 | function searchForDevice() { 274 | var deferred = Q.defer(); 275 | grunt.log.writeln("Searching for connected devices..."); 276 | 277 | (function check() { 278 | shell.exec("sdb", ["devices"]) 279 | .fail(function(e) { deferred.reject(e); }) 280 | .done(function(t) { 281 | var lines = t.split("\n"); 282 | if (lines.length < 2) { 283 | grunt.log.writeln("Device not found..."); 284 | setTimeout(check, 2000); 285 | } else { 286 | grunt.log.writeln("Device found: " + lines[1]); 287 | deferred.resolve(); 288 | } 289 | }); 290 | })(); 291 | 292 | return deferred.promise; 293 | } 294 | 295 | function initGruntTizen() { 296 | if (!gruntTizenInitialized) { 297 | gruntTizenInitialized = true; 298 | grunt.task.run("tizen_prepare"); 299 | } 300 | } 301 | 302 | function gruntTizenTask(action) { 303 | initGruntTizen(); 304 | grunt.task.run("tizen:"+action); 305 | } 306 | 307 | function removeWgtFile() { 308 | var wgtFile = getWgtName(); 309 | if (grunt.file.isFile(wgtFile)) { 310 | grunt.log.writeln("Removing old widget: " + wgtFile); 311 | fs.unlink(wgtFile); 312 | } 313 | } 314 | 315 | function setupWatcher() { 316 | var filesBeforeChange = null; 317 | 318 | grunt.event.on('watch', function(action, filepath, target) { 319 | if (target == "tizendev") { 320 | filesBeforeChange = util.getModifiedDates(getConfig().buildPath + "/"); 321 | 322 | var watchFunc = getConfig().fileChanged; 323 | if (watchFunc) 324 | watchFunc.apply(this, arguments); 325 | 326 | if (isCopyMatch(filepath)) 327 | copySrcFileToBuild(filepath); 328 | 329 | executeTasks(filepath); 330 | grunt.task.run("transferToPhone"); 331 | if (getConfig().liveReload) { 332 | grunt.task.run("tizendev:refreshBrowser"); 333 | } 334 | } 335 | }); 336 | 337 | grunt.registerTask("transferToPhone", function() { 338 | if (filesBeforeChange) { 339 | var filesAfterChange = util.getModifiedDates(getConfig().buildPath + "/"); 340 | var modifications = util.diffModifiedDates(filesBeforeChange, filesAfterChange); 341 | 342 | if (modifications.added.length > 0 || modifications.modified.length > 0) { 343 | var async = this.async(); 344 | transferToPhone(modifications) 345 | .then(tasks.restart) 346 | .fail(function(error) { 347 | grunt.fail.warn(error); 348 | }) 349 | .done(async); 350 | } 351 | } 352 | }); 353 | } 354 | 355 | function isCopyMatch(filepath) { 356 | var sourceRelativePath = path.relative(getConfig().sourceDir, filepath); 357 | var filter = _.flatten([getConfig().copy, getConfig().copyExclude]); 358 | return grunt.file.match(filter, sourceRelativePath).length > 0; 359 | } 360 | 361 | function executeTasks(filepath) { 362 | var sourceRelativePath = path.relative(getConfig().sourceDir, filepath); 363 | var tasks = getConfig().tasks; 364 | for (var taskName in tasks) { 365 | if (grunt.file.match(tasks[taskName], sourceRelativePath).length > 0) 366 | grunt.task.run(taskName); 367 | } 368 | } 369 | 370 | function executeAllTasks() { 371 | var tasks = getConfig().tasks; 372 | for (var taskName in tasks) { 373 | grunt.task.run(taskName); 374 | } 375 | } 376 | 377 | function copySrcFileToBuild(sourcePath) { 378 | var sourceFileRelativePath = path.relative(getConfig().sourceDir, sourcePath); 379 | var targetPath = path.join(getConfig().buildPath, sourceFileRelativePath); 380 | grunt.file.copy(sourcePath, targetPath); 381 | grunt.log.writeln("Copied " + sourcePath + " to " + targetPath); 382 | } 383 | 384 | function transferToPhone(modifications) { 385 | var changedFiles = _.flatten([modifications.added, modifications.modified]); 386 | var targetAppPath = grunt.template.process(getConfig().remoteAppLocation, {data: getConfig()}); 387 | var deferred = Q.defer(); 388 | 389 | var transfer = function(files) { 390 | if (files.length === 0) 391 | deferred.resolve(); 392 | else { 393 | var filepath = files.shift(); 394 | var relativePath = path.relative(getConfig().buildPath, filepath); 395 | var targetPath = path.join(targetAppPath, relativePath); 396 | 397 | shell.spawn({ 398 | cmd: "sdb", 399 | args: ["push", filepath, targetPath] 400 | }, function(error, output) { 401 | if (error) 402 | deferred.reject(error); 403 | else { 404 | grunt.log.writeln(output); 405 | transfer(files); 406 | } 407 | }); 408 | } 409 | }; 410 | 411 | shell.execSdb(["root", "on"]).then(function() { 412 | transfer(changedFiles); 413 | }); 414 | 415 | return deferred.promise; 416 | } 417 | 418 | function getSigningProfileName(profilesXmlPath) { 419 | var deferred = Q.defer(); 420 | var parse = require('xml2js').parseString; 421 | var profileName = getConfig().profile; 422 | 423 | if (profileName) { 424 | deferred.resolve(profileName); 425 | } else { 426 | parse(grunt.file.read(profilesXmlPath), {attrkey: "attr"}, function(err, result) { 427 | if (err) 428 | deferred.reject("Can't parse " + profilesXmlPath); 429 | else { 430 | try { 431 | deferred.resolve(_.first(result.profiles.profile).attr.name); 432 | } catch (e) { 433 | deferred.reject("Can't find profile in " + profilesXmlPath); 434 | } 435 | } 436 | }); 437 | } 438 | return deferred.promise; 439 | } 440 | 441 | // SETTINGS 442 | function getConfig() { 443 | return grunt.config("tizendev"); 444 | } 445 | 446 | function getProjectFilePath(file) { 447 | return path.join(getConfig().sourceDir, file); 448 | } 449 | 450 | function excludeBuildFolderFilter(config) { 451 | return "!" + path.join(path.relative(config.sourceDir, config.buildPath), "/**"); 452 | } 453 | 454 | function getWgtName() { 455 | var parts = getConfig().fullAppId.split("."); 456 | return path.join(getConfig().sourceDir, parts[1] + ".wgt"); 457 | } 458 | 459 | setupWatcher(); 460 | 461 | return tasks; 462 | }; -------------------------------------------------------------------------------- /fps-measurement/example_testcases/scroll_up_and_down.xmacro: -------------------------------------------------------------------------------- 1 | MotionNotify 360 1000 2 | ButtonPress 1 3 | MotionNotify 360 1000 4 | Delay 0.010 5 | MotionNotify 360 999 6 | Delay 0.010 7 | MotionNotify 360 996 8 | Delay 0.010 9 | MotionNotify 360 992 10 | Delay 0.010 11 | MotionNotify 360 987 12 | Delay 0.010 13 | MotionNotify 360 980 14 | Delay 0.010 15 | MotionNotify 360 971 16 | Delay 0.010 17 | MotionNotify 360 961 18 | Delay 0.010 19 | MotionNotify 360 950 20 | Delay 0.010 21 | MotionNotify 360 937 22 | Delay 0.010 23 | MotionNotify 360 923 24 | Delay 0.010 25 | MotionNotify 360 908 26 | Delay 0.010 27 | MotionNotify 360 891 28 | Delay 0.010 29 | MotionNotify 360 873 30 | Delay 0.010 31 | MotionNotify 360 854 32 | Delay 0.010 33 | MotionNotify 360 835 34 | Delay 0.010 35 | MotionNotify 360 814 36 | Delay 0.010 37 | MotionNotify 360 792 38 | Delay 0.010 39 | MotionNotify 360 770 40 | Delay 0.010 41 | MotionNotify 360 747 42 | Delay 0.010 43 | MotionNotify 360 723 44 | Delay 0.010 45 | MotionNotify 360 699 46 | Delay 0.010 47 | MotionNotify 360 674 48 | Delay 0.010 49 | MotionNotify 360 650 50 | Delay 0.010 51 | MotionNotify 360 625 52 | Delay 0.010 53 | MotionNotify 360 599 54 | Delay 0.010 55 | MotionNotify 360 574 56 | Delay 0.010 57 | MotionNotify 360 549 58 | Delay 0.010 59 | MotionNotify 360 525 60 | Delay 0.010 61 | MotionNotify 360 500 62 | Delay 0.010 63 | MotionNotify 360 476 64 | Delay 0.010 65 | MotionNotify 360 452 66 | Delay 0.010 67 | MotionNotify 360 429 68 | Delay 0.010 69 | MotionNotify 360 407 70 | Delay 0.010 71 | MotionNotify 360 385 72 | Delay 0.010 73 | MotionNotify 360 364 74 | Delay 0.010 75 | MotionNotify 360 345 76 | Delay 0.010 77 | MotionNotify 360 326 78 | Delay 0.010 79 | MotionNotify 360 308 80 | Delay 0.010 81 | MotionNotify 360 291 82 | Delay 0.010 83 | MotionNotify 360 276 84 | Delay 0.010 85 | MotionNotify 360 262 86 | Delay 0.010 87 | MotionNotify 360 249 88 | Delay 0.010 89 | MotionNotify 360 238 90 | Delay 0.010 91 | MotionNotify 360 228 92 | Delay 0.010 93 | MotionNotify 360 219 94 | Delay 0.010 95 | MotionNotify 360 212 96 | Delay 0.010 97 | MotionNotify 360 207 98 | Delay 0.010 99 | MotionNotify 360 203 100 | Delay 0.010 101 | MotionNotify 360 200 102 | Delay 0.010 103 | MotionNotify 360 200 104 | Delay 0.010 105 | MotionNotify 360 200 106 | Delay 0.010 107 | MotionNotify 360 203 108 | Delay 0.010 109 | MotionNotify 360 207 110 | Delay 0.010 111 | MotionNotify 360 212 112 | Delay 0.010 113 | MotionNotify 360 219 114 | Delay 0.010 115 | MotionNotify 360 228 116 | Delay 0.010 117 | MotionNotify 360 238 118 | Delay 0.010 119 | MotionNotify 360 249 120 | Delay 0.010 121 | MotionNotify 360 262 122 | Delay 0.010 123 | MotionNotify 360 276 124 | Delay 0.010 125 | MotionNotify 360 291 126 | Delay 0.010 127 | MotionNotify 360 308 128 | Delay 0.010 129 | MotionNotify 360 326 130 | Delay 0.010 131 | MotionNotify 360 345 132 | Delay 0.010 133 | MotionNotify 360 364 134 | Delay 0.010 135 | MotionNotify 360 385 136 | Delay 0.010 137 | MotionNotify 360 407 138 | Delay 0.010 139 | MotionNotify 360 429 140 | Delay 0.010 141 | MotionNotify 360 452 142 | Delay 0.010 143 | MotionNotify 360 476 144 | Delay 0.010 145 | MotionNotify 360 500 146 | Delay 0.010 147 | MotionNotify 360 525 148 | Delay 0.010 149 | MotionNotify 360 549 150 | Delay 0.010 151 | MotionNotify 360 574 152 | Delay 0.010 153 | MotionNotify 360 599 154 | Delay 0.010 155 | MotionNotify 360 625 156 | Delay 0.010 157 | MotionNotify 360 650 158 | Delay 0.010 159 | MotionNotify 360 674 160 | Delay 0.010 161 | MotionNotify 360 699 162 | Delay 0.010 163 | MotionNotify 360 723 164 | Delay 0.010 165 | MotionNotify 360 747 166 | Delay 0.010 167 | MotionNotify 360 770 168 | Delay 0.010 169 | MotionNotify 360 792 170 | Delay 0.010 171 | MotionNotify 360 814 172 | Delay 0.010 173 | MotionNotify 360 835 174 | Delay 0.010 175 | MotionNotify 360 854 176 | Delay 0.010 177 | MotionNotify 360 873 178 | Delay 0.010 179 | MotionNotify 360 891 180 | Delay 0.010 181 | MotionNotify 360 908 182 | Delay 0.010 183 | MotionNotify 360 923 184 | Delay 0.010 185 | MotionNotify 360 937 186 | Delay 0.010 187 | MotionNotify 360 950 188 | Delay 0.010 189 | MotionNotify 360 961 190 | Delay 0.010 191 | MotionNotify 360 971 192 | Delay 0.010 193 | MotionNotify 360 980 194 | Delay 0.010 195 | MotionNotify 360 987 196 | Delay 0.010 197 | MotionNotify 360 992 198 | Delay 0.010 199 | MotionNotify 360 996 200 | Delay 0.010 201 | MotionNotify 360 999 202 | Delay 0.010 203 | MotionNotify 360 1000 204 | Delay 0.010 205 | MotionNotify 360 999 206 | Delay 0.010 207 | MotionNotify 360 996 208 | Delay 0.010 209 | MotionNotify 360 992 210 | Delay 0.010 211 | MotionNotify 360 987 212 | Delay 0.010 213 | MotionNotify 360 980 214 | Delay 0.010 215 | MotionNotify 360 971 216 | Delay 0.010 217 | MotionNotify 360 961 218 | Delay 0.010 219 | MotionNotify 360 950 220 | Delay 0.010 221 | MotionNotify 360 937 222 | Delay 0.010 223 | MotionNotify 360 923 224 | Delay 0.010 225 | MotionNotify 360 908 226 | Delay 0.010 227 | MotionNotify 360 891 228 | Delay 0.010 229 | MotionNotify 360 873 230 | Delay 0.010 231 | MotionNotify 360 854 232 | Delay 0.010 233 | MotionNotify 360 835 234 | Delay 0.010 235 | MotionNotify 360 814 236 | Delay 0.010 237 | MotionNotify 360 792 238 | Delay 0.010 239 | MotionNotify 360 770 240 | Delay 0.010 241 | MotionNotify 360 747 242 | Delay 0.010 243 | MotionNotify 360 723 244 | Delay 0.010 245 | MotionNotify 360 699 246 | Delay 0.010 247 | MotionNotify 360 674 248 | Delay 0.010 249 | MotionNotify 360 650 250 | Delay 0.010 251 | MotionNotify 360 625 252 | Delay 0.010 253 | MotionNotify 360 600 254 | Delay 0.010 255 | MotionNotify 360 574 256 | Delay 0.010 257 | MotionNotify 360 549 258 | Delay 0.010 259 | MotionNotify 360 525 260 | Delay 0.010 261 | MotionNotify 360 500 262 | Delay 0.010 263 | MotionNotify 360 476 264 | Delay 0.010 265 | MotionNotify 360 452 266 | Delay 0.010 267 | MotionNotify 360 429 268 | Delay 0.010 269 | MotionNotify 360 407 270 | Delay 0.010 271 | MotionNotify 360 385 272 | Delay 0.010 273 | MotionNotify 360 364 274 | Delay 0.010 275 | MotionNotify 360 345 276 | Delay 0.010 277 | MotionNotify 360 326 278 | Delay 0.010 279 | MotionNotify 360 308 280 | Delay 0.010 281 | MotionNotify 360 291 282 | Delay 0.010 283 | MotionNotify 360 276 284 | Delay 0.010 285 | MotionNotify 360 262 286 | Delay 0.010 287 | MotionNotify 360 249 288 | Delay 0.010 289 | MotionNotify 360 238 290 | Delay 0.010 291 | MotionNotify 360 228 292 | Delay 0.010 293 | MotionNotify 360 219 294 | Delay 0.010 295 | MotionNotify 360 212 296 | Delay 0.010 297 | MotionNotify 360 207 298 | Delay 0.010 299 | MotionNotify 360 203 300 | Delay 0.010 301 | MotionNotify 360 200 302 | Delay 0.010 303 | MotionNotify 360 200 304 | Delay 0.010 305 | MotionNotify 360 200 306 | Delay 0.010 307 | MotionNotify 360 203 308 | Delay 0.010 309 | MotionNotify 360 207 310 | Delay 0.010 311 | MotionNotify 360 212 312 | Delay 0.010 313 | MotionNotify 360 219 314 | Delay 0.010 315 | MotionNotify 360 228 316 | Delay 0.010 317 | MotionNotify 360 238 318 | Delay 0.010 319 | MotionNotify 360 249 320 | Delay 0.010 321 | MotionNotify 360 262 322 | Delay 0.010 323 | MotionNotify 360 276 324 | Delay 0.010 325 | MotionNotify 360 291 326 | Delay 0.010 327 | MotionNotify 360 308 328 | Delay 0.010 329 | MotionNotify 360 326 330 | Delay 0.010 331 | MotionNotify 360 345 332 | Delay 0.010 333 | MotionNotify 360 364 334 | Delay 0.010 335 | MotionNotify 360 385 336 | Delay 0.010 337 | MotionNotify 360 407 338 | Delay 0.010 339 | MotionNotify 360 429 340 | Delay 0.010 341 | MotionNotify 360 452 342 | Delay 0.010 343 | MotionNotify 360 476 344 | Delay 0.010 345 | MotionNotify 360 500 346 | Delay 0.010 347 | MotionNotify 360 525 348 | Delay 0.010 349 | MotionNotify 360 549 350 | Delay 0.010 351 | MotionNotify 360 574 352 | Delay 0.010 353 | MotionNotify 360 599 354 | Delay 0.010 355 | MotionNotify 360 625 356 | Delay 0.010 357 | MotionNotify 360 650 358 | Delay 0.010 359 | MotionNotify 360 674 360 | Delay 0.010 361 | MotionNotify 360 699 362 | Delay 0.010 363 | MotionNotify 360 723 364 | Delay 0.010 365 | MotionNotify 360 747 366 | Delay 0.010 367 | MotionNotify 360 770 368 | Delay 0.010 369 | MotionNotify 360 792 370 | Delay 0.010 371 | MotionNotify 360 814 372 | Delay 0.010 373 | MotionNotify 360 835 374 | Delay 0.010 375 | MotionNotify 360 854 376 | Delay 0.010 377 | MotionNotify 360 873 378 | Delay 0.010 379 | MotionNotify 360 891 380 | Delay 0.010 381 | MotionNotify 360 908 382 | Delay 0.010 383 | MotionNotify 360 923 384 | Delay 0.010 385 | MotionNotify 360 937 386 | Delay 0.010 387 | MotionNotify 360 950 388 | Delay 0.010 389 | MotionNotify 360 961 390 | Delay 0.010 391 | MotionNotify 360 971 392 | Delay 0.010 393 | MotionNotify 360 980 394 | Delay 0.010 395 | MotionNotify 360 987 396 | Delay 0.010 397 | MotionNotify 360 992 398 | Delay 0.010 399 | MotionNotify 360 996 400 | Delay 0.010 401 | MotionNotify 360 999 402 | Delay 0.010 403 | MotionNotify 360 1000 404 | Delay 0.010 405 | MotionNotify 360 999 406 | Delay 0.010 407 | MotionNotify 360 996 408 | Delay 0.010 409 | MotionNotify 360 992 410 | Delay 0.010 411 | MotionNotify 360 987 412 | Delay 0.010 413 | MotionNotify 360 980 414 | Delay 0.010 415 | MotionNotify 360 971 416 | Delay 0.010 417 | MotionNotify 360 961 418 | Delay 0.010 419 | MotionNotify 360 950 420 | Delay 0.010 421 | MotionNotify 360 937 422 | Delay 0.010 423 | MotionNotify 360 923 424 | Delay 0.010 425 | MotionNotify 360 908 426 | Delay 0.010 427 | MotionNotify 360 891 428 | Delay 0.010 429 | MotionNotify 360 873 430 | Delay 0.010 431 | MotionNotify 360 854 432 | Delay 0.010 433 | MotionNotify 360 835 434 | Delay 0.010 435 | MotionNotify 360 814 436 | Delay 0.010 437 | MotionNotify 360 792 438 | Delay 0.010 439 | MotionNotify 360 770 440 | Delay 0.010 441 | MotionNotify 360 747 442 | Delay 0.010 443 | MotionNotify 360 723 444 | Delay 0.010 445 | MotionNotify 360 699 446 | Delay 0.010 447 | MotionNotify 360 674 448 | Delay 0.010 449 | MotionNotify 360 650 450 | Delay 0.010 451 | MotionNotify 360 625 452 | Delay 0.010 453 | MotionNotify 360 600 454 | Delay 0.010 455 | MotionNotify 360 574 456 | Delay 0.010 457 | MotionNotify 360 549 458 | Delay 0.010 459 | MotionNotify 360 525 460 | Delay 0.010 461 | MotionNotify 360 500 462 | Delay 0.010 463 | MotionNotify 360 476 464 | Delay 0.010 465 | MotionNotify 360 452 466 | Delay 0.010 467 | MotionNotify 360 429 468 | Delay 0.010 469 | MotionNotify 360 407 470 | Delay 0.010 471 | MotionNotify 360 385 472 | Delay 0.010 473 | MotionNotify 360 364 474 | Delay 0.010 475 | MotionNotify 360 345 476 | Delay 0.010 477 | MotionNotify 360 326 478 | Delay 0.010 479 | MotionNotify 360 308 480 | Delay 0.010 481 | MotionNotify 360 291 482 | Delay 0.010 483 | MotionNotify 360 276 484 | Delay 0.010 485 | MotionNotify 360 262 486 | Delay 0.010 487 | MotionNotify 360 249 488 | Delay 0.010 489 | MotionNotify 360 238 490 | Delay 0.010 491 | MotionNotify 360 228 492 | Delay 0.010 493 | MotionNotify 360 219 494 | Delay 0.010 495 | MotionNotify 360 212 496 | Delay 0.010 497 | MotionNotify 360 207 498 | Delay 0.010 499 | MotionNotify 360 203 500 | Delay 0.010 501 | MotionNotify 360 200 502 | Delay 0.010 503 | MotionNotify 360 200 504 | Delay 0.010 505 | MotionNotify 360 200 506 | Delay 0.010 507 | MotionNotify 360 203 508 | Delay 0.010 509 | MotionNotify 360 207 510 | Delay 0.010 511 | MotionNotify 360 212 512 | Delay 0.010 513 | MotionNotify 360 219 514 | Delay 0.010 515 | MotionNotify 360 228 516 | Delay 0.010 517 | MotionNotify 360 238 518 | Delay 0.010 519 | MotionNotify 360 249 520 | Delay 0.010 521 | MotionNotify 360 262 522 | Delay 0.010 523 | MotionNotify 360 276 524 | Delay 0.010 525 | MotionNotify 360 291 526 | Delay 0.010 527 | MotionNotify 360 308 528 | Delay 0.010 529 | MotionNotify 360 326 530 | Delay 0.010 531 | MotionNotify 360 345 532 | Delay 0.010 533 | MotionNotify 360 364 534 | Delay 0.010 535 | MotionNotify 360 385 536 | Delay 0.010 537 | MotionNotify 360 407 538 | Delay 0.010 539 | MotionNotify 360 429 540 | Delay 0.010 541 | MotionNotify 360 452 542 | Delay 0.010 543 | MotionNotify 360 476 544 | Delay 0.010 545 | MotionNotify 360 500 546 | Delay 0.010 547 | MotionNotify 360 525 548 | Delay 0.010 549 | MotionNotify 360 549 550 | Delay 0.010 551 | MotionNotify 360 574 552 | Delay 0.010 553 | MotionNotify 360 599 554 | Delay 0.010 555 | MotionNotify 360 625 556 | Delay 0.010 557 | MotionNotify 360 650 558 | Delay 0.010 559 | MotionNotify 360 674 560 | Delay 0.010 561 | MotionNotify 360 699 562 | Delay 0.010 563 | MotionNotify 360 723 564 | Delay 0.010 565 | MotionNotify 360 747 566 | Delay 0.010 567 | MotionNotify 360 770 568 | Delay 0.010 569 | MotionNotify 360 792 570 | Delay 0.010 571 | MotionNotify 360 814 572 | Delay 0.010 573 | MotionNotify 360 835 574 | Delay 0.010 575 | MotionNotify 360 854 576 | Delay 0.010 577 | MotionNotify 360 873 578 | Delay 0.010 579 | MotionNotify 360 891 580 | Delay 0.010 581 | MotionNotify 360 908 582 | Delay 0.010 583 | MotionNotify 360 923 584 | Delay 0.010 585 | MotionNotify 360 937 586 | Delay 0.010 587 | MotionNotify 360 950 588 | Delay 0.010 589 | MotionNotify 360 961 590 | Delay 0.010 591 | MotionNotify 360 971 592 | Delay 0.010 593 | MotionNotify 360 980 594 | Delay 0.010 595 | MotionNotify 360 987 596 | Delay 0.010 597 | MotionNotify 360 992 598 | Delay 0.010 599 | MotionNotify 360 996 600 | Delay 0.010 601 | MotionNotify 360 999 602 | Delay 0.010 603 | MotionNotify 360 1000 604 | Delay 0.010 605 | MotionNotify 360 999 606 | Delay 0.010 607 | MotionNotify 360 996 608 | Delay 0.010 609 | MotionNotify 360 992 610 | Delay 0.010 611 | MotionNotify 360 987 612 | Delay 0.010 613 | MotionNotify 360 980 614 | Delay 0.010 615 | MotionNotify 360 971 616 | Delay 0.010 617 | MotionNotify 360 961 618 | Delay 0.010 619 | MotionNotify 360 950 620 | Delay 0.010 621 | MotionNotify 360 937 622 | Delay 0.010 623 | MotionNotify 360 923 624 | Delay 0.010 625 | MotionNotify 360 908 626 | Delay 0.010 627 | MotionNotify 360 891 628 | Delay 0.010 629 | MotionNotify 360 873 630 | Delay 0.010 631 | MotionNotify 360 854 632 | Delay 0.010 633 | MotionNotify 360 835 634 | Delay 0.010 635 | MotionNotify 360 814 636 | Delay 0.010 637 | MotionNotify 360 792 638 | Delay 0.010 639 | MotionNotify 360 770 640 | Delay 0.010 641 | MotionNotify 360 747 642 | Delay 0.010 643 | MotionNotify 360 723 644 | Delay 0.010 645 | MotionNotify 360 699 646 | Delay 0.010 647 | MotionNotify 360 674 648 | Delay 0.010 649 | MotionNotify 360 650 650 | Delay 0.010 651 | MotionNotify 360 625 652 | Delay 0.010 653 | MotionNotify 360 600 654 | Delay 0.010 655 | MotionNotify 360 574 656 | Delay 0.010 657 | MotionNotify 360 549 658 | Delay 0.010 659 | MotionNotify 360 525 660 | Delay 0.010 661 | MotionNotify 360 500 662 | Delay 0.010 663 | MotionNotify 360 476 664 | Delay 0.010 665 | MotionNotify 360 452 666 | Delay 0.010 667 | MotionNotify 360 429 668 | Delay 0.010 669 | MotionNotify 360 407 670 | Delay 0.010 671 | MotionNotify 360 385 672 | Delay 0.010 673 | MotionNotify 360 364 674 | Delay 0.010 675 | MotionNotify 360 345 676 | Delay 0.010 677 | MotionNotify 360 326 678 | Delay 0.010 679 | MotionNotify 360 308 680 | Delay 0.010 681 | MotionNotify 360 291 682 | Delay 0.010 683 | MotionNotify 360 276 684 | Delay 0.010 685 | MotionNotify 360 262 686 | Delay 0.010 687 | MotionNotify 360 249 688 | Delay 0.010 689 | MotionNotify 360 238 690 | Delay 0.010 691 | MotionNotify 360 228 692 | Delay 0.010 693 | MotionNotify 360 219 694 | Delay 0.010 695 | MotionNotify 360 212 696 | Delay 0.010 697 | MotionNotify 360 207 698 | Delay 0.010 699 | MotionNotify 360 203 700 | Delay 0.010 701 | MotionNotify 360 200 702 | Delay 0.010 703 | MotionNotify 360 200 704 | Delay 0.010 705 | MotionNotify 360 200 706 | Delay 0.010 707 | MotionNotify 360 203 708 | Delay 0.010 709 | MotionNotify 360 207 710 | Delay 0.010 711 | MotionNotify 360 212 712 | Delay 0.010 713 | MotionNotify 360 219 714 | Delay 0.010 715 | MotionNotify 360 228 716 | Delay 0.010 717 | MotionNotify 360 238 718 | Delay 0.010 719 | MotionNotify 360 249 720 | Delay 0.010 721 | MotionNotify 360 262 722 | Delay 0.010 723 | MotionNotify 360 276 724 | Delay 0.010 725 | MotionNotify 360 291 726 | Delay 0.010 727 | MotionNotify 360 308 728 | Delay 0.010 729 | MotionNotify 360 326 730 | Delay 0.010 731 | MotionNotify 360 345 732 | Delay 0.010 733 | MotionNotify 360 364 734 | Delay 0.010 735 | MotionNotify 360 385 736 | Delay 0.010 737 | MotionNotify 360 407 738 | Delay 0.010 739 | MotionNotify 360 429 740 | Delay 0.010 741 | MotionNotify 360 452 742 | Delay 0.010 743 | MotionNotify 360 476 744 | Delay 0.010 745 | MotionNotify 360 500 746 | Delay 0.010 747 | MotionNotify 360 525 748 | Delay 0.010 749 | MotionNotify 360 549 750 | Delay 0.010 751 | MotionNotify 360 574 752 | Delay 0.010 753 | MotionNotify 360 599 754 | Delay 0.010 755 | MotionNotify 360 625 756 | Delay 0.010 757 | MotionNotify 360 650 758 | Delay 0.010 759 | MotionNotify 360 674 760 | Delay 0.010 761 | MotionNotify 360 699 762 | Delay 0.010 763 | MotionNotify 360 723 764 | Delay 0.010 765 | MotionNotify 360 747 766 | Delay 0.010 767 | MotionNotify 360 770 768 | Delay 0.010 769 | MotionNotify 360 792 770 | Delay 0.010 771 | MotionNotify 360 814 772 | Delay 0.010 773 | MotionNotify 360 835 774 | Delay 0.010 775 | MotionNotify 360 854 776 | Delay 0.010 777 | MotionNotify 360 873 778 | Delay 0.010 779 | MotionNotify 360 891 780 | Delay 0.010 781 | MotionNotify 360 908 782 | Delay 0.010 783 | MotionNotify 360 923 784 | Delay 0.010 785 | MotionNotify 360 937 786 | Delay 0.010 787 | MotionNotify 360 950 788 | Delay 0.010 789 | MotionNotify 360 961 790 | Delay 0.010 791 | MotionNotify 360 971 792 | Delay 0.010 793 | MotionNotify 360 980 794 | Delay 0.010 795 | MotionNotify 360 987 796 | Delay 0.010 797 | MotionNotify 360 992 798 | Delay 0.010 799 | MotionNotify 360 996 800 | Delay 0.010 801 | MotionNotify 360 999 802 | Delay 0.010 803 | MotionNotify 360 1000 804 | Delay 0.010 805 | MotionNotify 360 999 806 | Delay 0.010 807 | MotionNotify 360 996 808 | Delay 0.010 809 | MotionNotify 360 992 810 | Delay 0.010 811 | MotionNotify 360 987 812 | Delay 0.010 813 | MotionNotify 360 980 814 | Delay 0.010 815 | MotionNotify 360 971 816 | Delay 0.010 817 | MotionNotify 360 961 818 | Delay 0.010 819 | MotionNotify 360 950 820 | Delay 0.010 821 | MotionNotify 360 937 822 | Delay 0.010 823 | MotionNotify 360 923 824 | Delay 0.010 825 | MotionNotify 360 908 826 | Delay 0.010 827 | MotionNotify 360 891 828 | Delay 0.010 829 | MotionNotify 360 873 830 | Delay 0.010 831 | MotionNotify 360 854 832 | Delay 0.010 833 | MotionNotify 360 835 834 | Delay 0.010 835 | MotionNotify 360 814 836 | Delay 0.010 837 | MotionNotify 360 792 838 | Delay 0.010 839 | MotionNotify 360 770 840 | Delay 0.010 841 | MotionNotify 360 747 842 | Delay 0.010 843 | MotionNotify 360 723 844 | Delay 0.010 845 | MotionNotify 360 699 846 | Delay 0.010 847 | MotionNotify 360 674 848 | Delay 0.010 849 | MotionNotify 360 650 850 | Delay 0.010 851 | MotionNotify 360 625 852 | Delay 0.010 853 | MotionNotify 360 600 854 | Delay 0.010 855 | MotionNotify 360 574 856 | Delay 0.010 857 | MotionNotify 360 549 858 | Delay 0.010 859 | MotionNotify 360 525 860 | Delay 0.010 861 | MotionNotify 360 500 862 | Delay 0.010 863 | MotionNotify 360 476 864 | Delay 0.010 865 | MotionNotify 360 452 866 | Delay 0.010 867 | MotionNotify 360 429 868 | Delay 0.010 869 | MotionNotify 360 407 870 | Delay 0.010 871 | MotionNotify 360 385 872 | Delay 0.010 873 | MotionNotify 360 364 874 | Delay 0.010 875 | MotionNotify 360 345 876 | Delay 0.010 877 | MotionNotify 360 326 878 | Delay 0.010 879 | MotionNotify 360 308 880 | Delay 0.010 881 | MotionNotify 360 291 882 | Delay 0.010 883 | MotionNotify 360 276 884 | Delay 0.010 885 | MotionNotify 360 262 886 | Delay 0.010 887 | MotionNotify 360 249 888 | Delay 0.010 889 | MotionNotify 360 238 890 | Delay 0.010 891 | MotionNotify 360 228 892 | Delay 0.010 893 | MotionNotify 360 219 894 | Delay 0.010 895 | MotionNotify 360 212 896 | Delay 0.010 897 | MotionNotify 360 207 898 | Delay 0.010 899 | MotionNotify 360 203 900 | Delay 0.010 901 | MotionNotify 360 200 902 | Delay 0.010 903 | MotionNotify 360 200 904 | Delay 0.010 905 | MotionNotify 360 200 906 | Delay 0.010 907 | MotionNotify 360 203 908 | Delay 0.010 909 | MotionNotify 360 207 910 | Delay 0.010 911 | MotionNotify 360 212 912 | Delay 0.010 913 | MotionNotify 360 219 914 | Delay 0.010 915 | MotionNotify 360 228 916 | Delay 0.010 917 | MotionNotify 360 238 918 | Delay 0.010 919 | MotionNotify 360 249 920 | Delay 0.010 921 | MotionNotify 360 262 922 | Delay 0.010 923 | MotionNotify 360 276 924 | Delay 0.010 925 | MotionNotify 360 291 926 | Delay 0.010 927 | MotionNotify 360 308 928 | Delay 0.010 929 | MotionNotify 360 326 930 | Delay 0.010 931 | MotionNotify 360 345 932 | Delay 0.010 933 | MotionNotify 360 364 934 | Delay 0.010 935 | MotionNotify 360 385 936 | Delay 0.010 937 | MotionNotify 360 407 938 | Delay 0.010 939 | MotionNotify 360 429 940 | Delay 0.010 941 | MotionNotify 360 452 942 | Delay 0.010 943 | MotionNotify 360 476 944 | Delay 0.010 945 | MotionNotify 360 500 946 | Delay 0.010 947 | MotionNotify 360 525 948 | Delay 0.010 949 | MotionNotify 360 549 950 | Delay 0.010 951 | MotionNotify 360 574 952 | Delay 0.010 953 | MotionNotify 360 599 954 | Delay 0.010 955 | MotionNotify 360 625 956 | Delay 0.010 957 | MotionNotify 360 650 958 | Delay 0.010 959 | MotionNotify 360 674 960 | Delay 0.010 961 | MotionNotify 360 699 962 | Delay 0.010 963 | MotionNotify 360 723 964 | Delay 0.010 965 | MotionNotify 360 747 966 | Delay 0.010 967 | MotionNotify 360 770 968 | Delay 0.010 969 | MotionNotify 360 792 970 | Delay 0.010 971 | MotionNotify 360 814 972 | Delay 0.010 973 | MotionNotify 360 835 974 | Delay 0.010 975 | MotionNotify 360 854 976 | Delay 0.010 977 | MotionNotify 360 873 978 | Delay 0.010 979 | MotionNotify 360 891 980 | Delay 0.010 981 | MotionNotify 360 908 982 | Delay 0.010 983 | MotionNotify 360 923 984 | Delay 0.010 985 | MotionNotify 360 937 986 | Delay 0.010 987 | MotionNotify 360 950 988 | Delay 0.010 989 | MotionNotify 360 961 990 | Delay 0.010 991 | MotionNotify 360 971 992 | Delay 0.010 993 | MotionNotify 360 980 994 | Delay 0.010 995 | MotionNotify 360 987 996 | Delay 0.010 997 | MotionNotify 360 992 998 | Delay 0.010 999 | MotionNotify 360 996 1000 | Delay 0.010 1001 | MotionNotify 360 999 1002 | Delay 0.010 1003 | MotionNotify 360 1000 1004 | Delay 0.010 1005 | MotionNotify 360 999 1006 | Delay 0.010 1007 | MotionNotify 360 996 1008 | Delay 0.010 1009 | MotionNotify 360 992 1010 | Delay 0.010 1011 | MotionNotify 360 987 1012 | Delay 0.010 1013 | MotionNotify 360 980 1014 | Delay 0.010 1015 | MotionNotify 360 971 1016 | Delay 0.010 1017 | MotionNotify 360 961 1018 | Delay 0.010 1019 | MotionNotify 360 950 1020 | Delay 0.010 1021 | MotionNotify 360 937 1022 | Delay 0.010 1023 | MotionNotify 360 923 1024 | Delay 0.010 1025 | MotionNotify 360 908 1026 | Delay 0.010 1027 | MotionNotify 360 891 1028 | Delay 0.010 1029 | MotionNotify 360 873 1030 | Delay 0.010 1031 | MotionNotify 360 854 1032 | Delay 0.010 1033 | MotionNotify 360 835 1034 | Delay 0.010 1035 | MotionNotify 360 814 1036 | Delay 0.010 1037 | MotionNotify 360 792 1038 | Delay 0.010 1039 | MotionNotify 360 770 1040 | Delay 0.010 1041 | MotionNotify 360 747 1042 | Delay 0.010 1043 | MotionNotify 360 723 1044 | Delay 0.010 1045 | MotionNotify 360 699 1046 | Delay 0.010 1047 | MotionNotify 360 674 1048 | Delay 0.010 1049 | MotionNotify 360 650 1050 | Delay 0.010 1051 | MotionNotify 360 625 1052 | Delay 0.010 1053 | MotionNotify 360 600 1054 | Delay 0.010 1055 | MotionNotify 360 574 1056 | Delay 0.010 1057 | MotionNotify 360 549 1058 | Delay 0.010 1059 | MotionNotify 360 525 1060 | Delay 0.010 1061 | MotionNotify 360 500 1062 | Delay 0.010 1063 | MotionNotify 360 476 1064 | Delay 0.010 1065 | MotionNotify 360 452 1066 | Delay 0.010 1067 | MotionNotify 360 429 1068 | Delay 0.010 1069 | MotionNotify 360 407 1070 | Delay 0.010 1071 | MotionNotify 360 385 1072 | Delay 0.010 1073 | MotionNotify 360 364 1074 | Delay 0.010 1075 | MotionNotify 360 345 1076 | Delay 0.010 1077 | MotionNotify 360 326 1078 | Delay 0.010 1079 | MotionNotify 360 308 1080 | Delay 0.010 1081 | MotionNotify 360 291 1082 | Delay 0.010 1083 | MotionNotify 360 276 1084 | Delay 0.010 1085 | MotionNotify 360 262 1086 | Delay 0.010 1087 | MotionNotify 360 249 1088 | Delay 0.010 1089 | MotionNotify 360 238 1090 | Delay 0.010 1091 | MotionNotify 360 228 1092 | Delay 0.010 1093 | MotionNotify 360 219 1094 | Delay 0.010 1095 | MotionNotify 360 212 1096 | Delay 0.010 1097 | MotionNotify 360 207 1098 | Delay 0.010 1099 | MotionNotify 360 203 1100 | Delay 0.010 1101 | MotionNotify 360 200 1102 | Delay 0.010 1103 | MotionNotify 360 200 1104 | Delay 0.010 1105 | MotionNotify 360 200 1106 | Delay 0.010 1107 | MotionNotify 360 203 1108 | Delay 0.010 1109 | MotionNotify 360 207 1110 | Delay 0.010 1111 | MotionNotify 360 212 1112 | Delay 0.010 1113 | MotionNotify 360 219 1114 | Delay 0.010 1115 | MotionNotify 360 228 1116 | Delay 0.010 1117 | MotionNotify 360 238 1118 | Delay 0.010 1119 | MotionNotify 360 249 1120 | Delay 0.010 1121 | MotionNotify 360 262 1122 | Delay 0.010 1123 | MotionNotify 360 276 1124 | Delay 0.010 1125 | MotionNotify 360 291 1126 | Delay 0.010 1127 | MotionNotify 360 308 1128 | Delay 0.010 1129 | MotionNotify 360 326 1130 | Delay 0.010 1131 | MotionNotify 360 345 1132 | Delay 0.010 1133 | MotionNotify 360 364 1134 | Delay 0.010 1135 | MotionNotify 360 385 1136 | Delay 0.010 1137 | MotionNotify 360 407 1138 | Delay 0.010 1139 | MotionNotify 360 429 1140 | Delay 0.010 1141 | MotionNotify 360 452 1142 | Delay 0.010 1143 | MotionNotify 360 476 1144 | Delay 0.010 1145 | MotionNotify 360 500 1146 | Delay 0.010 1147 | MotionNotify 360 525 1148 | Delay 0.010 1149 | MotionNotify 360 549 1150 | Delay 0.010 1151 | MotionNotify 360 574 1152 | Delay 0.010 1153 | MotionNotify 360 600 1154 | Delay 0.010 1155 | MotionNotify 360 625 1156 | Delay 0.010 1157 | MotionNotify 360 650 1158 | Delay 0.010 1159 | MotionNotify 360 674 1160 | Delay 0.010 1161 | MotionNotify 360 699 1162 | Delay 0.010 1163 | MotionNotify 360 723 1164 | Delay 0.010 1165 | MotionNotify 360 747 1166 | Delay 0.010 1167 | MotionNotify 360 770 1168 | Delay 0.010 1169 | MotionNotify 360 792 1170 | Delay 0.010 1171 | MotionNotify 360 814 1172 | Delay 0.010 1173 | MotionNotify 360 835 1174 | Delay 0.010 1175 | MotionNotify 360 854 1176 | Delay 0.010 1177 | MotionNotify 360 873 1178 | Delay 0.010 1179 | MotionNotify 360 891 1180 | Delay 0.010 1181 | MotionNotify 360 908 1182 | Delay 0.010 1183 | MotionNotify 360 923 1184 | Delay 0.010 1185 | MotionNotify 360 937 1186 | Delay 0.010 1187 | MotionNotify 360 950 1188 | Delay 0.010 1189 | MotionNotify 360 961 1190 | Delay 0.010 1191 | MotionNotify 360 971 1192 | Delay 0.010 1193 | MotionNotify 360 980 1194 | Delay 0.010 1195 | MotionNotify 360 987 1196 | Delay 0.010 1197 | MotionNotify 360 992 1198 | Delay 0.010 1199 | MotionNotify 360 996 1200 | Delay 0.010 1201 | MotionNotify 360 999 1202 | Delay 0.010 1203 | MotionNotify 360 1000 1204 | Delay 0.010 1205 | MotionNotify 360 999 1206 | Delay 0.010 1207 | MotionNotify 360 996 1208 | Delay 0.010 1209 | MotionNotify 360 992 1210 | Delay 0.010 1211 | MotionNotify 360 987 1212 | Delay 0.010 1213 | MotionNotify 360 980 1214 | Delay 0.010 1215 | MotionNotify 360 971 1216 | Delay 0.010 1217 | MotionNotify 360 961 1218 | Delay 0.010 1219 | MotionNotify 360 950 1220 | Delay 0.010 1221 | MotionNotify 360 937 1222 | Delay 0.010 1223 | MotionNotify 360 923 1224 | Delay 0.010 1225 | MotionNotify 360 908 1226 | Delay 0.010 1227 | MotionNotify 360 891 1228 | Delay 0.010 1229 | MotionNotify 360 873 1230 | Delay 0.010 1231 | MotionNotify 360 854 1232 | Delay 0.010 1233 | MotionNotify 360 835 1234 | Delay 0.010 1235 | MotionNotify 360 814 1236 | Delay 0.010 1237 | MotionNotify 360 792 1238 | Delay 0.010 1239 | MotionNotify 360 770 1240 | Delay 0.010 1241 | MotionNotify 360 747 1242 | Delay 0.010 1243 | MotionNotify 360 723 1244 | Delay 0.010 1245 | MotionNotify 360 699 1246 | Delay 0.010 1247 | MotionNotify 360 674 1248 | Delay 0.010 1249 | MotionNotify 360 650 1250 | Delay 0.010 1251 | MotionNotify 360 625 1252 | Delay 0.010 1253 | MotionNotify 360 599 1254 | Delay 0.010 1255 | MotionNotify 360 574 1256 | Delay 0.010 1257 | MotionNotify 360 549 1258 | Delay 0.010 1259 | MotionNotify 360 525 1260 | Delay 0.010 1261 | MotionNotify 360 500 1262 | Delay 0.010 1263 | MotionNotify 360 476 1264 | Delay 0.010 1265 | MotionNotify 360 452 1266 | Delay 0.010 1267 | MotionNotify 360 429 1268 | Delay 0.010 1269 | MotionNotify 360 407 1270 | Delay 0.010 1271 | MotionNotify 360 385 1272 | Delay 0.010 1273 | MotionNotify 360 364 1274 | Delay 0.010 1275 | MotionNotify 360 345 1276 | Delay 0.010 1277 | MotionNotify 360 326 1278 | Delay 0.010 1279 | MotionNotify 360 308 1280 | Delay 0.010 1281 | MotionNotify 360 291 1282 | Delay 0.010 1283 | MotionNotify 360 276 1284 | Delay 0.010 1285 | MotionNotify 360 262 1286 | Delay 0.010 1287 | MotionNotify 360 249 1288 | Delay 0.010 1289 | MotionNotify 360 238 1290 | Delay 0.010 1291 | MotionNotify 360 228 1292 | Delay 0.010 1293 | MotionNotify 360 219 1294 | Delay 0.010 1295 | MotionNotify 360 212 1296 | Delay 0.010 1297 | MotionNotify 360 207 1298 | Delay 0.010 1299 | MotionNotify 360 203 1300 | Delay 0.010 1301 | MotionNotify 360 200 1302 | Delay 0.010 1303 | MotionNotify 360 200 1304 | Delay 0.010 1305 | MotionNotify 360 200 1306 | Delay 0.010 1307 | MotionNotify 360 203 1308 | Delay 0.010 1309 | MotionNotify 360 207 1310 | Delay 0.010 1311 | MotionNotify 360 212 1312 | Delay 0.010 1313 | MotionNotify 360 219 1314 | Delay 0.010 1315 | MotionNotify 360 228 1316 | Delay 0.010 1317 | MotionNotify 360 238 1318 | Delay 0.010 1319 | MotionNotify 360 249 1320 | Delay 0.010 1321 | MotionNotify 360 262 1322 | Delay 0.010 1323 | MotionNotify 360 276 1324 | Delay 0.010 1325 | MotionNotify 360 291 1326 | Delay 0.010 1327 | MotionNotify 360 308 1328 | Delay 0.010 1329 | MotionNotify 360 326 1330 | Delay 0.010 1331 | MotionNotify 360 345 1332 | Delay 0.010 1333 | MotionNotify 360 364 1334 | Delay 0.010 1335 | MotionNotify 360 385 1336 | Delay 0.010 1337 | MotionNotify 360 407 1338 | Delay 0.010 1339 | MotionNotify 360 429 1340 | Delay 0.010 1341 | MotionNotify 360 452 1342 | Delay 0.010 1343 | MotionNotify 360 476 1344 | Delay 0.010 1345 | MotionNotify 360 500 1346 | Delay 0.010 1347 | MotionNotify 360 525 1348 | Delay 0.010 1349 | MotionNotify 360 549 1350 | Delay 0.010 1351 | MotionNotify 360 574 1352 | Delay 0.010 1353 | MotionNotify 360 600 1354 | Delay 0.010 1355 | MotionNotify 360 625 1356 | Delay 0.010 1357 | MotionNotify 360 650 1358 | Delay 0.010 1359 | MotionNotify 360 674 1360 | Delay 0.010 1361 | MotionNotify 360 699 1362 | Delay 0.010 1363 | MotionNotify 360 723 1364 | Delay 0.010 1365 | MotionNotify 360 747 1366 | Delay 0.010 1367 | MotionNotify 360 770 1368 | Delay 0.010 1369 | MotionNotify 360 792 1370 | Delay 0.010 1371 | MotionNotify 360 814 1372 | Delay 0.010 1373 | MotionNotify 360 835 1374 | Delay 0.010 1375 | MotionNotify 360 854 1376 | Delay 0.010 1377 | MotionNotify 360 873 1378 | Delay 0.010 1379 | MotionNotify 360 891 1380 | Delay 0.010 1381 | MotionNotify 360 908 1382 | Delay 0.010 1383 | MotionNotify 360 923 1384 | Delay 0.010 1385 | MotionNotify 360 937 1386 | Delay 0.010 1387 | MotionNotify 360 950 1388 | Delay 0.010 1389 | MotionNotify 360 961 1390 | Delay 0.010 1391 | MotionNotify 360 971 1392 | Delay 0.010 1393 | MotionNotify 360 980 1394 | Delay 0.010 1395 | MotionNotify 360 987 1396 | Delay 0.010 1397 | MotionNotify 360 992 1398 | Delay 0.010 1399 | MotionNotify 360 996 1400 | Delay 0.010 1401 | MotionNotify 360 999 1402 | Delay 0.010 1403 | MotionNotify 360 1000 1404 | Delay 0.010 1405 | MotionNotify 360 999 1406 | Delay 0.010 1407 | MotionNotify 360 996 1408 | Delay 0.010 1409 | MotionNotify 360 992 1410 | Delay 0.010 1411 | MotionNotify 360 987 1412 | Delay 0.010 1413 | MotionNotify 360 980 1414 | Delay 0.010 1415 | MotionNotify 360 971 1416 | Delay 0.010 1417 | MotionNotify 360 961 1418 | Delay 0.010 1419 | MotionNotify 360 950 1420 | Delay 0.010 1421 | MotionNotify 360 937 1422 | Delay 0.010 1423 | MotionNotify 360 923 1424 | Delay 0.010 1425 | MotionNotify 360 908 1426 | Delay 0.010 1427 | MotionNotify 360 891 1428 | Delay 0.010 1429 | MotionNotify 360 873 1430 | Delay 0.010 1431 | MotionNotify 360 854 1432 | Delay 0.010 1433 | MotionNotify 360 835 1434 | Delay 0.010 1435 | MotionNotify 360 814 1436 | Delay 0.010 1437 | MotionNotify 360 792 1438 | Delay 0.010 1439 | MotionNotify 360 770 1440 | Delay 0.010 1441 | MotionNotify 360 747 1442 | Delay 0.010 1443 | MotionNotify 360 723 1444 | Delay 0.010 1445 | MotionNotify 360 699 1446 | Delay 0.010 1447 | MotionNotify 360 674 1448 | Delay 0.010 1449 | MotionNotify 360 650 1450 | Delay 0.010 1451 | MotionNotify 360 625 1452 | Delay 0.010 1453 | MotionNotify 360 599 1454 | Delay 0.010 1455 | MotionNotify 360 574 1456 | Delay 0.010 1457 | MotionNotify 360 549 1458 | Delay 0.010 1459 | MotionNotify 360 525 1460 | Delay 0.010 1461 | MotionNotify 360 500 1462 | Delay 0.010 1463 | MotionNotify 360 476 1464 | Delay 0.010 1465 | MotionNotify 360 452 1466 | Delay 0.010 1467 | MotionNotify 360 429 1468 | Delay 0.010 1469 | MotionNotify 360 407 1470 | Delay 0.010 1471 | MotionNotify 360 385 1472 | Delay 0.010 1473 | MotionNotify 360 364 1474 | Delay 0.010 1475 | MotionNotify 360 345 1476 | Delay 0.010 1477 | MotionNotify 360 326 1478 | Delay 0.010 1479 | MotionNotify 360 308 1480 | Delay 0.010 1481 | MotionNotify 360 291 1482 | Delay 0.010 1483 | MotionNotify 360 276 1484 | Delay 0.010 1485 | MotionNotify 360 262 1486 | Delay 0.010 1487 | MotionNotify 360 249 1488 | Delay 0.010 1489 | MotionNotify 360 238 1490 | Delay 0.010 1491 | MotionNotify 360 228 1492 | Delay 0.010 1493 | MotionNotify 360 219 1494 | Delay 0.010 1495 | MotionNotify 360 212 1496 | Delay 0.010 1497 | MotionNotify 360 207 1498 | Delay 0.010 1499 | MotionNotify 360 203 1500 | Delay 0.010 1501 | MotionNotify 360 200 1502 | Delay 0.010 1503 | MotionNotify 360 200 1504 | Delay 0.010 1505 | MotionNotify 360 200 1506 | Delay 0.010 1507 | MotionNotify 360 203 1508 | Delay 0.010 1509 | MotionNotify 360 207 1510 | Delay 0.010 1511 | MotionNotify 360 212 1512 | Delay 0.010 1513 | MotionNotify 360 219 1514 | Delay 0.010 1515 | MotionNotify 360 228 1516 | Delay 0.010 1517 | MotionNotify 360 238 1518 | Delay 0.010 1519 | MotionNotify 360 249 1520 | Delay 0.010 1521 | MotionNotify 360 262 1522 | Delay 0.010 1523 | MotionNotify 360 276 1524 | Delay 0.010 1525 | MotionNotify 360 291 1526 | Delay 0.010 1527 | MotionNotify 360 308 1528 | Delay 0.010 1529 | MotionNotify 360 326 1530 | Delay 0.010 1531 | MotionNotify 360 345 1532 | Delay 0.010 1533 | MotionNotify 360 364 1534 | Delay 0.010 1535 | MotionNotify 360 385 1536 | Delay 0.010 1537 | MotionNotify 360 407 1538 | Delay 0.010 1539 | MotionNotify 360 429 1540 | Delay 0.010 1541 | MotionNotify 360 452 1542 | Delay 0.010 1543 | MotionNotify 360 476 1544 | Delay 0.010 1545 | MotionNotify 360 500 1546 | Delay 0.010 1547 | MotionNotify 360 525 1548 | Delay 0.010 1549 | MotionNotify 360 549 1550 | Delay 0.010 1551 | MotionNotify 360 574 1552 | Delay 0.010 1553 | MotionNotify 360 600 1554 | Delay 0.010 1555 | MotionNotify 360 625 1556 | Delay 0.010 1557 | MotionNotify 360 650 1558 | Delay 0.010 1559 | MotionNotify 360 674 1560 | Delay 0.010 1561 | MotionNotify 360 699 1562 | Delay 0.010 1563 | MotionNotify 360 723 1564 | Delay 0.010 1565 | MotionNotify 360 747 1566 | Delay 0.010 1567 | MotionNotify 360 770 1568 | Delay 0.010 1569 | MotionNotify 360 792 1570 | Delay 0.010 1571 | MotionNotify 360 814 1572 | Delay 0.010 1573 | MotionNotify 360 835 1574 | Delay 0.010 1575 | MotionNotify 360 854 1576 | Delay 0.010 1577 | MotionNotify 360 873 1578 | Delay 0.010 1579 | MotionNotify 360 891 1580 | Delay 0.010 1581 | MotionNotify 360 908 1582 | Delay 0.010 1583 | MotionNotify 360 923 1584 | Delay 0.010 1585 | MotionNotify 360 937 1586 | Delay 0.010 1587 | MotionNotify 360 950 1588 | Delay 0.010 1589 | MotionNotify 360 961 1590 | Delay 0.010 1591 | MotionNotify 360 971 1592 | Delay 0.010 1593 | MotionNotify 360 980 1594 | Delay 0.010 1595 | MotionNotify 360 987 1596 | Delay 0.010 1597 | MotionNotify 360 992 1598 | Delay 0.010 1599 | MotionNotify 360 996 1600 | Delay 0.010 1601 | MotionNotify 360 999 1602 | Delay 0.010 1603 | MotionNotify 360 1000 1604 | Delay 0.010 1605 | MotionNotify 360 999 1606 | Delay 0.010 1607 | MotionNotify 360 996 1608 | Delay 0.010 1609 | MotionNotify 360 992 1610 | Delay 0.010 1611 | MotionNotify 360 987 1612 | Delay 0.010 1613 | MotionNotify 360 980 1614 | Delay 0.010 1615 | MotionNotify 360 971 1616 | Delay 0.010 1617 | MotionNotify 360 961 1618 | Delay 0.010 1619 | MotionNotify 360 950 1620 | Delay 0.010 1621 | MotionNotify 360 937 1622 | Delay 0.010 1623 | MotionNotify 360 923 1624 | Delay 0.010 1625 | MotionNotify 360 908 1626 | Delay 0.010 1627 | MotionNotify 360 891 1628 | Delay 0.010 1629 | MotionNotify 360 873 1630 | Delay 0.010 1631 | MotionNotify 360 854 1632 | Delay 0.010 1633 | MotionNotify 360 835 1634 | Delay 0.010 1635 | MotionNotify 360 814 1636 | Delay 0.010 1637 | MotionNotify 360 792 1638 | Delay 0.010 1639 | MotionNotify 360 770 1640 | Delay 0.010 1641 | MotionNotify 360 747 1642 | Delay 0.010 1643 | MotionNotify 360 723 1644 | Delay 0.010 1645 | MotionNotify 360 699 1646 | Delay 0.010 1647 | MotionNotify 360 674 1648 | Delay 0.010 1649 | MotionNotify 360 650 1650 | Delay 0.010 1651 | MotionNotify 360 625 1652 | Delay 0.010 1653 | MotionNotify 360 599 1654 | Delay 0.010 1655 | MotionNotify 360 574 1656 | Delay 0.010 1657 | MotionNotify 360 549 1658 | Delay 0.010 1659 | MotionNotify 360 525 1660 | Delay 0.010 1661 | MotionNotify 360 500 1662 | Delay 0.010 1663 | MotionNotify 360 476 1664 | Delay 0.010 1665 | MotionNotify 360 452 1666 | Delay 0.010 1667 | MotionNotify 360 429 1668 | Delay 0.010 1669 | MotionNotify 360 407 1670 | Delay 0.010 1671 | MotionNotify 360 385 1672 | Delay 0.010 1673 | MotionNotify 360 364 1674 | Delay 0.010 1675 | MotionNotify 360 345 1676 | Delay 0.010 1677 | MotionNotify 360 326 1678 | Delay 0.010 1679 | MotionNotify 360 308 1680 | Delay 0.010 1681 | MotionNotify 360 291 1682 | Delay 0.010 1683 | MotionNotify 360 276 1684 | Delay 0.010 1685 | MotionNotify 360 262 1686 | Delay 0.010 1687 | MotionNotify 360 249 1688 | Delay 0.010 1689 | MotionNotify 360 238 1690 | Delay 0.010 1691 | MotionNotify 360 228 1692 | Delay 0.010 1693 | MotionNotify 360 219 1694 | Delay 0.010 1695 | MotionNotify 360 212 1696 | Delay 0.010 1697 | MotionNotify 360 207 1698 | Delay 0.010 1699 | MotionNotify 360 203 1700 | Delay 0.010 1701 | MotionNotify 360 200 1702 | Delay 0.010 1703 | MotionNotify 360 200 1704 | Delay 0.010 1705 | MotionNotify 360 200 1706 | Delay 0.010 1707 | MotionNotify 360 203 1708 | Delay 0.010 1709 | MotionNotify 360 207 1710 | Delay 0.010 1711 | MotionNotify 360 212 1712 | Delay 0.010 1713 | MotionNotify 360 219 1714 | Delay 0.010 1715 | MotionNotify 360 228 1716 | Delay 0.010 1717 | MotionNotify 360 238 1718 | Delay 0.010 1719 | MotionNotify 360 249 1720 | Delay 0.010 1721 | MotionNotify 360 262 1722 | Delay 0.010 1723 | MotionNotify 360 276 1724 | Delay 0.010 1725 | MotionNotify 360 291 1726 | Delay 0.010 1727 | MotionNotify 360 308 1728 | Delay 0.010 1729 | MotionNotify 360 326 1730 | Delay 0.010 1731 | MotionNotify 360 345 1732 | Delay 0.010 1733 | MotionNotify 360 364 1734 | Delay 0.010 1735 | MotionNotify 360 385 1736 | Delay 0.010 1737 | MotionNotify 360 407 1738 | Delay 0.010 1739 | MotionNotify 360 429 1740 | Delay 0.010 1741 | MotionNotify 360 452 1742 | Delay 0.010 1743 | MotionNotify 360 476 1744 | Delay 0.010 1745 | MotionNotify 360 500 1746 | Delay 0.010 1747 | MotionNotify 360 525 1748 | Delay 0.010 1749 | MotionNotify 360 549 1750 | Delay 0.010 1751 | MotionNotify 360 574 1752 | Delay 0.010 1753 | MotionNotify 360 600 1754 | Delay 0.010 1755 | MotionNotify 360 625 1756 | Delay 0.010 1757 | MotionNotify 360 650 1758 | Delay 0.010 1759 | MotionNotify 360 674 1760 | Delay 0.010 1761 | MotionNotify 360 699 1762 | Delay 0.010 1763 | MotionNotify 360 723 1764 | Delay 0.010 1765 | MotionNotify 360 747 1766 | Delay 0.010 1767 | MotionNotify 360 770 1768 | Delay 0.010 1769 | MotionNotify 360 792 1770 | Delay 0.010 1771 | MotionNotify 360 814 1772 | Delay 0.010 1773 | MotionNotify 360 835 1774 | Delay 0.010 1775 | MotionNotify 360 854 1776 | Delay 0.010 1777 | MotionNotify 360 873 1778 | Delay 0.010 1779 | MotionNotify 360 891 1780 | Delay 0.010 1781 | MotionNotify 360 908 1782 | Delay 0.010 1783 | MotionNotify 360 923 1784 | Delay 0.010 1785 | MotionNotify 360 937 1786 | Delay 0.010 1787 | MotionNotify 360 950 1788 | Delay 0.010 1789 | MotionNotify 360 961 1790 | Delay 0.010 1791 | MotionNotify 360 971 1792 | Delay 0.010 1793 | MotionNotify 360 980 1794 | Delay 0.010 1795 | MotionNotify 360 987 1796 | Delay 0.010 1797 | MotionNotify 360 992 1798 | Delay 0.010 1799 | MotionNotify 360 996 1800 | Delay 0.010 1801 | MotionNotify 360 999 1802 | Delay 0.010 1803 | MotionNotify 360 1000 1804 | Delay 0.010 1805 | MotionNotify 360 999 1806 | Delay 0.010 1807 | MotionNotify 360 996 1808 | Delay 0.010 1809 | MotionNotify 360 992 1810 | Delay 0.010 1811 | MotionNotify 360 987 1812 | Delay 0.010 1813 | MotionNotify 360 980 1814 | Delay 0.010 1815 | MotionNotify 360 971 1816 | Delay 0.010 1817 | MotionNotify 360 961 1818 | Delay 0.010 1819 | MotionNotify 360 950 1820 | Delay 0.010 1821 | MotionNotify 360 937 1822 | Delay 0.010 1823 | MotionNotify 360 923 1824 | Delay 0.010 1825 | MotionNotify 360 908 1826 | Delay 0.010 1827 | MotionNotify 360 891 1828 | Delay 0.010 1829 | MotionNotify 360 873 1830 | Delay 0.010 1831 | MotionNotify 360 854 1832 | Delay 0.010 1833 | MotionNotify 360 835 1834 | Delay 0.010 1835 | MotionNotify 360 814 1836 | Delay 0.010 1837 | MotionNotify 360 792 1838 | Delay 0.010 1839 | MotionNotify 360 770 1840 | Delay 0.010 1841 | MotionNotify 360 747 1842 | Delay 0.010 1843 | MotionNotify 360 723 1844 | Delay 0.010 1845 | MotionNotify 360 699 1846 | Delay 0.010 1847 | MotionNotify 360 674 1848 | Delay 0.010 1849 | MotionNotify 360 650 1850 | Delay 0.010 1851 | MotionNotify 360 625 1852 | Delay 0.010 1853 | MotionNotify 360 599 1854 | Delay 0.010 1855 | MotionNotify 360 574 1856 | Delay 0.010 1857 | MotionNotify 360 549 1858 | Delay 0.010 1859 | MotionNotify 360 525 1860 | Delay 0.010 1861 | MotionNotify 360 500 1862 | Delay 0.010 1863 | MotionNotify 360 476 1864 | Delay 0.010 1865 | MotionNotify 360 452 1866 | Delay 0.010 1867 | MotionNotify 360 429 1868 | Delay 0.010 1869 | MotionNotify 360 407 1870 | Delay 0.010 1871 | MotionNotify 360 385 1872 | Delay 0.010 1873 | MotionNotify 360 364 1874 | Delay 0.010 1875 | MotionNotify 360 345 1876 | Delay 0.010 1877 | MotionNotify 360 326 1878 | Delay 0.010 1879 | MotionNotify 360 308 1880 | Delay 0.010 1881 | MotionNotify 360 291 1882 | Delay 0.010 1883 | MotionNotify 360 276 1884 | Delay 0.010 1885 | MotionNotify 360 262 1886 | Delay 0.010 1887 | MotionNotify 360 249 1888 | Delay 0.010 1889 | MotionNotify 360 238 1890 | Delay 0.010 1891 | MotionNotify 360 228 1892 | Delay 0.010 1893 | MotionNotify 360 219 1894 | Delay 0.010 1895 | MotionNotify 360 212 1896 | Delay 0.010 1897 | MotionNotify 360 207 1898 | Delay 0.010 1899 | MotionNotify 360 203 1900 | Delay 0.010 1901 | MotionNotify 360 200 1902 | Delay 0.010 1903 | MotionNotify 360 200 1904 | Delay 0.010 1905 | MotionNotify 360 200 1906 | Delay 0.010 1907 | MotionNotify 360 203 1908 | Delay 0.010 1909 | MotionNotify 360 207 1910 | Delay 0.010 1911 | MotionNotify 360 212 1912 | Delay 0.010 1913 | MotionNotify 360 219 1914 | Delay 0.010 1915 | MotionNotify 360 228 1916 | Delay 0.010 1917 | MotionNotify 360 238 1918 | Delay 0.010 1919 | MotionNotify 360 249 1920 | Delay 0.010 1921 | MotionNotify 360 262 1922 | Delay 0.010 1923 | MotionNotify 360 276 1924 | Delay 0.010 1925 | MotionNotify 360 291 1926 | Delay 0.010 1927 | MotionNotify 360 308 1928 | Delay 0.010 1929 | MotionNotify 360 326 1930 | Delay 0.010 1931 | MotionNotify 360 345 1932 | Delay 0.010 1933 | MotionNotify 360 364 1934 | Delay 0.010 1935 | MotionNotify 360 385 1936 | Delay 0.010 1937 | MotionNotify 360 407 1938 | Delay 0.010 1939 | MotionNotify 360 429 1940 | Delay 0.010 1941 | MotionNotify 360 452 1942 | Delay 0.010 1943 | MotionNotify 360 476 1944 | Delay 0.010 1945 | MotionNotify 360 500 1946 | Delay 0.010 1947 | MotionNotify 360 525 1948 | Delay 0.010 1949 | MotionNotify 360 549 1950 | Delay 0.010 1951 | MotionNotify 360 574 1952 | Delay 0.010 1953 | MotionNotify 360 600 1954 | Delay 0.010 1955 | MotionNotify 360 625 1956 | Delay 0.010 1957 | MotionNotify 360 650 1958 | Delay 0.010 1959 | MotionNotify 360 674 1960 | Delay 0.010 1961 | MotionNotify 360 699 1962 | Delay 0.010 1963 | MotionNotify 360 723 1964 | Delay 0.010 1965 | MotionNotify 360 747 1966 | Delay 0.010 1967 | MotionNotify 360 770 1968 | Delay 0.010 1969 | MotionNotify 360 792 1970 | Delay 0.010 1971 | MotionNotify 360 814 1972 | Delay 0.010 1973 | MotionNotify 360 835 1974 | Delay 0.010 1975 | MotionNotify 360 854 1976 | Delay 0.010 1977 | MotionNotify 360 873 1978 | Delay 0.010 1979 | MotionNotify 360 891 1980 | Delay 0.010 1981 | MotionNotify 360 908 1982 | Delay 0.010 1983 | MotionNotify 360 923 1984 | Delay 0.010 1985 | MotionNotify 360 937 1986 | Delay 0.010 1987 | MotionNotify 360 950 1988 | Delay 0.010 1989 | MotionNotify 360 961 1990 | Delay 0.010 1991 | MotionNotify 360 971 1992 | Delay 0.010 1993 | MotionNotify 360 980 1994 | Delay 0.010 1995 | MotionNotify 360 987 1996 | Delay 0.010 1997 | MotionNotify 360 992 1998 | Delay 0.010 1999 | MotionNotify 360 996 2000 | Delay 0.010 2001 | MotionNotify 360 999 2002 | Delay 0.010 2003 | ButtonRelease 1 2004 | --------------------------------------------------------------------------------