blob: 70ed6a8b137f865863ba6bf28a69390323e2443d [file] [log] [blame]
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"fmt"
"io"
"io/ioutil"
"math"
"os"
"regexp"
"strconv"
"strings"
"sync"
"time"
"android.googlesource.com/platform/tools/gpu/client/adb"
"android.googlesource.com/platform/tools/gpu/framework/log"
"android.googlesource.com/platform/tools/gpu/framework/task"
)
// The input file is essentially a sparse table of integer values.
type inputProperty string
type inputEvent map[inputProperty]int
type inputEvents []inputEvent
var inputProperties []inputProperty
func registerInputPropery(prop string) inputProperty {
inputProperties = append(inputProperties, inputProperty(prop))
return inputProperty(prop)
}
var (
// Display dimensions
kOrientation = registerInputPropery("orientation")
kWidth = registerInputPropery("width")
kHeight = registerInputPropery("height")
// Touch-screen dimensions
kMinX = registerInputPropery("minX")
kMaxX = registerInputPropery("maxX")
kMinY = registerInputPropery("minY")
kMaxY = registerInputPropery("maxY")
// Frame statistics
kTime = registerInputPropery("time")
kFrame = registerInputPropery("frame")
kDrawsPerFrame = registerInputPropery("drawsPerFrame")
// Screen tap/swipe
kX = registerInputPropery("x")
kY = registerInputPropery("y")
kPressed = registerInputPropery("pressed")
// End of recording
kEnd = registerInputPropery("end")
)
func writeEvent(out io.Writer, event inputEvent) {
var line []string
for _, name := range inputProperties {
if value, ok := event[name]; ok {
line = append(line, fmt.Sprintf("%s:%v", name, value))
}
}
fmt.Fprintf(out, "%s\n", strings.Join(line, " "))
}
func parseEvent(line string) (inputEvent, error) {
input := inputEvent{}
for _, kvp := range strings.Split(line, " ") {
if kvp != "" {
parts := strings.Split(kvp, ":")
if len(parts) == 2 {
name := inputProperty(parts[0])
value, err := strconv.Atoi(parts[1])
if err != nil {
return nil, err
}
input[name] = value
} else {
return nil, fmt.Errorf("Failed to parse key-value pair: '%s'", kvp)
}
}
}
return input, nil
}
// Implementation of the Writer interface which forwards the text to a lambda.
type lambdaWriter struct {
f func(s string)
}
func (w *lambdaWriter) Write(p []byte) (n int, err error) {
w.f(string(p))
return len(p), nil
}
func atoi(s string) int {
v, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return v
}
type frameInfo struct{ frame, drawsPerFrame int }
// Monitor logcat and parse frame statistics.
func monitorFrameStatistics(ctx log.Context, d adb.Device, out chan frameInfo) {
re := regexp.MustCompile("NumFrames:([0-9]+).*NumDrawsPerFrame:([0-9]+)")
stdout := &lambdaWriter{f: func(s string) {
for _, match := range re.FindAllStringSubmatch(s, -1) {
out <- frameInfo{frame: atoi(match[1]), drawsPerFrame: atoi(match[2])}
}
}}
d.Command("logcat", "-T", "1", "-s", "GAPID:I").Capture(stdout, nil).Run(ctx)
close(out)
}
type currentFrameInfo struct {
value frameInfo
mutex sync.Mutex
}
// Observe channel and make copy of the most recent value.
// We need to make copy of the channel to observe it.
func (info *currentFrameInfo) update(in <-chan frameInfo, out chan frameInfo) {
for v := range in {
info.mutex.Lock()
info.value = v
info.mutex.Unlock()
if out != nil {
out <- v
}
}
if out != nil {
close(out)
}
}
func (info *currentFrameInfo) get() frameInfo {
info.mutex.Lock()
v := info.value
info.mutex.Unlock()
return v
}
// Write frame statistics into a file (rate limited).
func recordFrameStatistics(out io.Writer, in <-chan frameInfo) {
const rate_limit = 10
const min_change = 0.2
startTime := time.Now()
nextFrame, lastDrawsPerFrame := 0, 0
for info := range in {
// Emit statistics only if the number of draws changed significantly
// and if it has been at least couple of frame since last time.
change := float64(info.drawsPerFrame+1)/float64(lastDrawsPerFrame+1) - 1.0
if info.frame >= nextFrame && math.Abs(change) >= min_change {
t := int(time.Now().Sub(startTime).Seconds() * 1000)
writeEvent(out, inputEvent{kTime: t, kFrame: info.frame, kDrawsPerFrame: info.drawsPerFrame})
nextFrame = info.frame + rate_limit
lastDrawsPerFrame = info.drawsPerFrame
}
}
}
type touchInfo struct{ x, y, pressed int }
// Monitor and parse touch screen events.
func monitorTouchScreen(ctx log.Context, d adb.Device, out chan touchInfo) {
x, y, pressed := 0, 0, 0
stdout := &lambdaWriter{f: func(s string) {
for _, line := range strings.Split(s, "\n") {
var device_id, value int
var event_type, event_code string
if _, err := fmt.Sscanf(line, "/dev/input/event%d: %s %s %x",
&device_id, &event_type, &event_code, &value); err == nil {
switch {
case event_type == "EV_ABS" && event_code == "ABS_MT_POSITION_X":
x, pressed = value, 1
case event_type == "EV_ABS" && event_code == "ABS_MT_POSITION_Y":
y, pressed = value, 1
case event_type == "EV_SYN" && event_code == "SYN_REPORT":
out <- touchInfo{x: x, y: y, pressed: pressed}
pressed = 0
}
}
}
}}
d.Shell("getevent", "-l").Capture(stdout, nil).Run(ctx)
close(out)
}
// Write touch-screen events to file (rate limited).
// The touch event will also include the most recent frame information.
func recordTouchInfo(out io.Writer, currentInfo *currentFrameInfo, in <-chan touchInfo) {
const rate_limit = 10
startTime := time.Now()
wasPressed, nextPressedFrame := 0, 0
for info := range in {
frameInfo := currentInfo.get()
if wasPressed != info.pressed || (info.pressed == 1 && frameInfo.frame >= nextPressedFrame) {
wasPressed = info.pressed
t := int(time.Now().Sub(startTime).Seconds() * 1000)
writeEvent(out, inputEvent{kTime: t,
kFrame: frameInfo.frame, kDrawsPerFrame: frameInfo.drawsPerFrame,
kX: info.x, kY: info.y, kPressed: info.pressed})
nextPressedFrame = frameInfo.frame + rate_limit
}
}
}
func startRecordingInputs(ctx log.Context, d adb.Device, filename string) (cleanup func(), err error) {
out, err := os.Create(filename)
if err != nil {
return nil, err
}
fmt.Fprintf(out, "# %s\n", strings.Join(os.Args, " "))
// Record screen dimensions
if orientation, width, height, ok := d.GetScreenDimensions(ctx); ok {
writeEvent(out, inputEvent{kOrientation: orientation, kWidth: width, kHeight: height})
}
// Record touch dimensions
if _, minX, maxX, minY, maxY, ok := d.GetTouchDimensions(ctx); ok {
writeEvent(out, inputEvent{kMinX: minX, kMaxX: maxX, kMinY: minY, kMaxY: maxY})
}
touchInfos := make(chan touchInfo, 256)
frameInfos := make(chan frameInfo, 256)
frameInfosCopy := make(chan frameInfo, 256)
stats := &currentFrameInfo{}
go monitorTouchScreen(ctx, d, touchInfos)
go monitorFrameStatistics(ctx, d, frameInfos)
go stats.update(frameInfos, frameInfosCopy)
go recordTouchInfo(out, stats, touchInfos)
go recordFrameStatistics(out, frameInfosCopy)
startTime := time.Now()
return func() {
t := int(time.Now().Sub(startTime).Seconds() * 1000)
writeEvent(out, inputEvent{kTime: t, kEnd: 1})
}, nil
}
func loadReplayInputs(filename string) (inputs inputEvents, err error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
for _, line := range strings.Split(string(data), "\n") {
if !strings.HasPrefix(line, "#") {
input, err := parseEvent(line)
if err != nil {
return nil, err
}
inputs = append(inputs, input)
}
}
return
}
// Load given file and start replaying the user inputs.
// 'eof' will be signalled when the end of file is reached.
func startReplayingInputs(ctx log.Context, d adb.Device, replayInputsIn string, stop task.CancelFunc) error {
inputs, err := loadReplayInputs(replayInputsIn)
if err != nil {
return err
}
deviceId, _, maxX, _, maxY, ok := d.GetTouchDimensions(ctx)
if !ok {
return fmt.Errorf("Faild to get touchscreen dimensions")
}
stats := currentFrameInfo{}
frameInfos := make(chan frameInfo, 256)
go monitorFrameStatistics(ctx, d, frameInfos)
go stats.update(frameInfos, nil)
go func() {
ctx := ctx.Enter("Inputs")
info := ctx.Info().Logf
startTime := time.Now()
time_drift := time.Duration(0)
value_of := inputEvent{} // Keep track of most recent state.
for _, input := range inputs {
for k, v := range input {
value_of[k] = v
}
if target, ok := input[kTime]; ok {
// Wait for the minimum time
current := time.Now().Sub(startTime)
target := time.Duration(target)*time.Millisecond + time_drift
if current < target {
info("Wait until %.1fs", target.Seconds())
time.Sleep(target - current)
} else {
time_drift = time_drift + current - target
info("Time drift: %.1fs", time_drift.Seconds())
}
}
if target, ok := input[kFrame]; ok {
// Wait for the minimum frame
for stats.get().frame < target {
time_drift = time_drift + time.Second
info("Wait for more frames (%v seen, %v needed, %.1fs drift)",
stats.get().frame, target, time_drift.Seconds())
time.Sleep(time.Second)
}
}
if target, ok := input[kDrawsPerFrame]; ok {
// Wait for the minimum draw count
target = target - target/10 - 1
for stats.get().drawsPerFrame < target {
time_drift = time_drift + time.Second
info("Wait for more draws (%v seen, %v needed, %.1fs drift)",
stats.get().drawsPerFrame, target, time_drift.Seconds())
time.Sleep(time.Second)
target = target - target/10 - 1 // relax over time to ensure progress
}
}
if pressed, ok := input[kPressed]; ok {
// Press/release screen
x, y := input[kX]*maxX/value_of[kMaxX], input[kY]*maxY/value_of[kMaxY]
info("Touch: x:%v y:%v pressed:%v", x, y, pressed)
d.SendTouch(ctx, deviceId, x, y, pressed != 0)
}
if end, ok := input[kEnd]; ok && end == 1 {
stop()
return
}
}
}()
return nil
}