提交 94184606 编写于 作者: T Trevor Johns

Merge branch 'google/master'

Conflicts:
- LICENSE
- historian.py
# How to become a contributor and submit your own code
## Contributor License Agreements
We'd love to accept your sample apps and patches! Before we can take them, we
have to jump a couple of legal hurdles.
Please fill out either the individual or corporate Contributor License Agreement
(CLA).
* If you are an individual writing original source code and you're sure you
own the intellectual property, then you'll need to sign an [individual CLA]
(https://developers.google.com/open-source/cla/individual).
* If you work for a company that wants to allow you to contribute your work,
then you'll need to sign a [corporate CLA]
(https://developers.google.com/open-source/cla/corporate).
Follow either of the two links above to access the appropriate CLA and
instructions for how to sign and return it. Once we receive it, we'll be able to
accept your pull requests.
## Contributing A Patch
1. Submit an issue describing your proposed change to the repo in question.
2. The repo owner will respond to your issue promptly.
3. If your proposed change is accepted, and you haven't already done so, sign a
Contributor License Agreement (see details above).
4. Fork the desired repo, develop and test your code changes.
5. Ensure that your code adheres to the existing style in the sample to which
you are contributing. Refer to the
[Google Cloud Platform Samples Style Guide]
(https://github.com/GoogleCloudPlatform/Template/wiki/style.html) for the
recommended coding standards for this organization.
6. Ensure that your code has an appropriate set of unit tests which all pass.
7. Submit a pull request.
Battery Historian 2.0
=====================
Battery Historian is a tool to inspect battery related information and events on an Android device (Android 5.0 Lollipop and later: API Level 21+) while the device was on battery. It allows application developers to visualize system and application level events on a timeline and easily see various aggregated statistics since the device was last fully charged.
Introduction
------------
Battery Historian 2.0 is a complete rewrite in Go and uses some JavaScript visualization libraries to display battery related events on a timeline with panning and zooming functionality. In addition, v2.0 allows developers to pick an application and inspect the metrics that impact battery specific to the chosen application.
Getting Started
---------------
If you are new to the Go programming language:
* Follow the instructions available at <http://golang.org/doc/install> for downloading and installing the Go compilers, tools, and libraries.
* Create a workspace directory according to the instructions at
<http://golang.org/doc/code.html#Organization> and ensure that `GOPATH` and
`GOBIN` environment variables are appropriately set and added to your `$PATH`
environment variable. `$GOBIN should be set to $GOPATH/bin`.
Next, install Go support for Protocol Buffers by running go get.
```
# Grab the code from the repository and install the proto package.
$ go get -u github.com/golang/protobuf/proto
$ go get -u github.com/golang/protobuf/protoc-gen-go
```
The compiler plugin, protoc-gen-go, will be installed in $GOBIN, which must be
in your $PATH for the protocol compiler, protoc, to find it.
Next, download the Battery Historian 2.0 code:
```
# Download Battery Historian 2.0
$ go get -u github.com/google/battery-historian
$ cd $GOPATH/src/github.com/google/battery-historian
# Compile Javascript files using the Closure compiler
$ bash setup.sh
# Run Historian on your machine (make sure $PATH contains $GOBIN)
$ go run cmd/battery-historian/battery-historian.go [--port <default:9999>]
```
Remember, you must always run battery-historian from inside the `$GOPATH/src/github.com/google/battery-historian` directory:
```
cd $GOPATH/src/github.com/google/battery-historian
go run cmd/battery-historian/battery-historian.go [--port <default:9999>]
```
#### How to take a bug report
To take a bug report from your Android device, you will need to enable USB debugging under `Settings > System > Developer Options`. On Android 4.2 and higher, the Developer options screen is hidden by default. You can enable this by following the instructions [here](<http://developer.android.com/tools/help/adb.html#Enabling>).
Next, to obtain a bug report from your development device
```
$ adb bugreport > bugreport.txt
```
### Start analyzing!
You are all set now. Run `historian` and visit <http://localhost:9999> and upload the `bugreport.txt` file to start analyzing.
By default, Android does not record timestamps for application-specific
userspace wakelock transitions even though aggregate statistics are maintained
on a running basis. If you want Historian to display detailed information about
each individual wakelock on the timeline, you should enable full wakelock reporting using the following command before starting your experiment:
```
adb shell dumpsys batterystats --enable full-wake-history
```
Note that by enabling full wakelock reporting the battery history log overflows
in a few hours. Use this option for short test runs (3-4 hrs).
To reset aggregated battery stats and timeline at the beginning of a measurement:
```
adb shell dumpsys batterystats --reset
```
Screenshots
-----------
![Visualization](/screenshots/viz.png "Timeline Visualization")
![System](/screenshots/stats.png "Aggregated System statistics since the device was last fully charged")
![App](/screenshots/app.png "Application specific statistics")
Advanced
--------
The following information is for advanced users only who are interested in modifying the code.
##### Modifying the proto files
If you modify the proto files (pb/\*/\*.proto), you will need to regenerate the compiled Go output files using `regen_proto.sh`.
##### Other command line tools
```
# System stats
$ go run exec/local_checkin_parse.go --input=bugreport.txt
# Timeline analysis
$ go run exec/local_history_parse.go --summary=totalTime --input=bugreport.txt
```
Support
-------
- G+ Community (Discussion Thread: Battery Historian): https://plus.google.com/b/108967384991768947849/communities/114791428968349268860
If you've found an error in this sample, please file an issue:
<https://github.com/google/battery-historian/issues>
Patches are encouraged, and may be submitted by forking this project and
submitting a pull request through GitHub.
License
-------
Copyright 2015 Google, Inc.
Licensed to the Apache Software Foundation (ASF) under one or more contributor
license agreements. See the NOTICE file distributed with this work for
additional information regarding copyright ownership. The ASF licenses this
file to you 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.
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 analyzer runs the Historian state machine code on the uploaded bugreport.
package analyzer
import (
"bufio"
"bytes"
"errors"
"fmt"
"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"github.com/golang/protobuf/proto"
"github.com/google/battery-historian/checkinparse"
"github.com/google/battery-historian/checkinutil"
"github.com/google/battery-historian/packageutils"
"github.com/google/battery-historian/parseutils"
"github.com/google/battery-historian/presenter"
bspb "github.com/google/battery-historian/pb/batterystats_proto"
sessionpb "github.com/google/battery-historian/pb/session_proto"
)
const (
maxFileSize = 50 * 1024 * 1024 // 50 MB Limit
minSupportedSDK = 21 // We only support Lollipop bug reports and above
)
var (
// Initialized in InitTemplates()
uploadTempl *template.Template
resultTempl *template.Template
isOptimizedJs bool
)
type historianData struct {
html string
err error
}
type summariesData struct {
summaries []parseutils.ActivitySummary
historianCsv string
errs []error
}
type checkinData struct {
batterystats *bspb.BatteryStats
warnings []string
err []error
}
// InitTemplates initializes the HTML templates.
func InitTemplates() {
uploadTempl = template.Must(template.ParseFiles(
"templates/base.html", "templates/body.html", "templates/upload.html"),
)
// base.html is intentionally excluded from resultTempl. resultTempl is loaded into the HTML
// generated by uploadTempl, so attempting to include base.html here causes some of the
// javascript files to be imported twice, which causes things to start blowing up.
resultTempl = template.Must(template.ParseFiles(
"templates/body.html", "templates/summaries.html"),
)
}
// SetIsOptimized sets whether the JS will be optimized.
func SetIsOptimized(optimized bool) {
isOptimizedJs = optimized
}
func closeConnection(w http.ResponseWriter, s string) {
if flusher, ok := w.(http.Flusher); ok {
w.Header().Set("Connection", "close")
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(s)))
w.WriteHeader(http.StatusExpectationFailed)
io.WriteString(w, s)
flusher.Flush()
}
fmt.Println(s, " Closing connection.")
conn, _, _ := w.(http.Hijacker).Hijack()
conn.Close()
}
// UploadHandler is the main analysis function.
func UploadHandler(w http.ResponseWriter, r *http.Request) {
// If false, the upload template will load closure and js files in the header.
uploadData := struct {
IsOptimizedJs bool
}{
isOptimizedJs,
}
switch r.Method {
//GET displays the upload form.
case "GET":
if err := uploadTempl.Execute(w, uploadData); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
//POST takes the uploaded file(s) and saves it to disk.
case "POST":
// Do not accept files that are greater than 50 MBs
if r.ContentLength > maxFileSize {
closeConnection(w, "File too large (>50MB).")
return
}
r.Body = http.MaxBytesReader(w, r.Body, maxFileSize)
//get the multipart reader for the request.
reader, err := r.MultipartReader()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Printf("Trace Starting reading uploaded file. %d bytes", r.ContentLength)
//copy each part to destination.
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
//if part.FileName() is empty, skip this iteration.
if part == nil || part.FileName() == "" {
continue
}
b, err := ioutil.ReadAll(part)
if err != nil {
http.Error(w, "Failed to read file. Please try again.", http.StatusInternalServerError)
return
}
contentType := http.DetectContentType(b)
if !strings.Contains(contentType, "text/plain") {
http.Error(w, "Incorrect file format detected", http.StatusInternalServerError)
return
}
log.Printf("Trace started analyzing file.")
// Generate the Historian plot and parsing simultaneously.
historianCh := make(chan historianData)
summariesCh := make(chan summariesData)
checkinCh := make(chan checkinData)
contents := string(b)
// Create a temporary file to save bug report, for the Historian script.
tmpFile, err := ioutil.TempFile("", "historian")
historianOutput := historianData{"", err}
if err == nil {
// Don't run the Historian script if could not create temporary file.
fname := tmpFile.Name()
defer os.Remove(fname)
tmpFile.WriteString(contents)
tmpFile.Close()
go func() {
html, err := generateHistorianPlot(w, part.FileName(), fname)
historianCh <- historianData{html, err}
log.Printf("Trace finished generating Historian plot.")
}()
}
var errs []error
sdk, err := sdkVersion(contents)
if sdk < minSupportedSDK {
errs = append(errs, errors.New("unsupported bug report version"))
}
if err != nil {
errs = append(errs, err)
}
if sdk >= minSupportedSDK {
// No point running these if we don't support the sdk version since we won't get any data from them.
go func() {
o, c, errs := analyze(contents)
summariesCh <- summariesData{o, c, errs}
log.Printf("Trace finished processing summary data.")
}()
go func() {
var ctr checkinutil.IntCounter
/* Extract Build Fingerprint from the bugreport. */
s := &sessionpb.Checkin{
Checkin: proto.String(contents),
BuildFingerprint: proto.String(extractBuildFingerprint(contents)),
}
pkgs, pkgErrs := packageutils.ExtractAppsFromBugReport(contents)
stats, warnings, pbsErrs := checkinparse.ParseBatteryStats(&ctr, checkinparse.CreateCheckinReport(s), pkgs)
checkinCh <- checkinData{stats, warnings, append(pkgErrs, pbsErrs...)}
}()
}
if historianOutput.err == nil {
historianOutput = <-historianCh
}
if historianOutput.err != nil {
historianOutput.html = fmt.Sprintf("Error generating historian plot: %v", historianOutput.err)
}
var summariesOutput summariesData
var checkinOutput checkinData
if sdk >= minSupportedSDK {
summariesOutput = <-summariesCh
checkinOutput = <-checkinCh
errs = append(errs, append(summariesOutput.errs, checkinOutput.err...)...)
}
log.Printf("Trace finished generating Historian plot and summaries.")
data := presenter.Data(sdk, modelName(contents), summariesOutput.historianCsv, part.FileName(), summariesOutput.summaries, checkinOutput.batterystats, historianOutput.html, checkinOutput.warnings, errs)
if err := resultTempl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
log.Printf("Trace ended analyzing file.")
}
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func analyze(bugreport string) ([]parseutils.ActivitySummary, string, []error) {
var b bytes.Buffer
rep := parseutils.AnalyzeHistory(bugreport, parseutils.FormatTotalTime, &b, false)
// Exclude summaries with no change in battery level
var a []parseutils.ActivitySummary
for _, s := range rep.Summaries {
if s.InitialBatteryLevel != s.FinalBatteryLevel {
a = append(a, s)
}
}
return a, b.String(), rep.Errs
}
// generateHistorianPlot calls the Historian python script to generate html charts.
func generateHistorianPlot(w http.ResponseWriter, reportName, filepath string) (string, error) {
cmd := exec.Command("./historian.py", "-c", "-m", "-r", reportName, filepath)
// Stdout pipe for reading the generated Historian output.
cmdStdout, stdoutErr := cmd.StdoutPipe()
if stdoutErr != nil {
return "", stdoutErr
}
// Run the Historian script.
if err := cmd.Start(); err != nil {
return "", err
}
outputCh := make(chan string)
go getStdout(w, cmdStdout, outputCh)
// Read the output generated by historian.
output := <-outputCh
if err := cmd.Wait(); err != nil {
return "", err
}
return output, nil
}
// getStdout reads the output generated by Historian.
func getStdout(w http.ResponseWriter, stdout io.ReadCloser, outputChan chan string) {
scanner := bufio.NewScanner(stdout)
var buffer bytes.Buffer
for scanner.Scan() {
buffer.WriteString(scanner.Text() + "\n")
}
outputChan <- buffer.String()
}
func sdkVersion(input string) (int, error) {
// Found in the System Properties section of a bug report.
re := regexp.MustCompile(`.*\[ro.build.version.sdk\]:\s+\[(?P<sdkVersion>\d+)\].*`)
if match, result := parseutils.SubexpNames(re, input); match {
return strconv.Atoi(result["sdkVersion"])
}
return -1, errors.New("unable to find device SDK version")
}
// modelName returns the device's model name (ie. Nexus 5).
func modelName(input string) string {
// Found in the System Properties section of a bug report.
re := regexp.MustCompile(`.*\[ro.product.model\]:\s+\[(?P<modelName>.*)\].*`)
if match, result := parseutils.SubexpNames(re, input); match {
return result["modelName"]
}
// We should only get to this point in the case of a bad (malformed) bug report.
return "unknown device"
}
func extractBuildFingerprint(input string) string {
// A regular expression to match any build fingerprint line in the bugreport.
re := regexp.MustCompile("Build\\s+fingerprint:\\s+'(?P<build>\\S+)'")
var out string
for _, line := range strings.Split(input, "\n") {
if match, result := parseutils.SubexpNames(re, line); match {
out = result["build"]
break
}
}
return out
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 build provides functions for dealing with Android build fingerprints.
package build
import (
"regexp"
"strings"
"github.com/golang/protobuf/proto"
pb "github.com/google/battery-historian/pb/build_proto"
)
var fingerprintRE = regexp.MustCompile(
`^([^/]+)/([^/]+)/([^:]+):([^/]+)/([^/]+)/([^:]+):([^/]+)/([^/]+)`)
func Build(f string) *pb.Build {
b := &pb.Build{}
b.Fingerprint = proto.String(f)
if m := fingerprintRE.FindStringSubmatch(f); len(m) == 9 {
b.Brand = proto.String(m[1])
b.Product = proto.String(m[2])
b.Device = proto.String(m[3])
b.Release = proto.String(m[4])
b.BuildId = proto.String(m[5])
b.Incremental = proto.String(m[6])
b.Type = proto.String(m[7])
b.Tags = strings.Split(m[8], ",")
}
return b
}
此差异已折叠。
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 checkinutil contains common/utility functions and data structures
// that are used in parsing of checkin format.
package checkinutil
// ChildInfo contains linkage information for App.Child.
type ChildInfo struct {
// predefined parent name (e.g., GOOGLE_SERVICES for gms and gsf)
Parent string
// true if the app is HeadChild
Head bool
}
// CheckinReport is a lightweight struct (compared to BatteryStats proto) to store Android checkin
// reports including batterystats and package manager dumps.
type CheckinReport struct {
TimeUsec int64 // End time, therefore, the time this report was taken.
TimeZone string
AndroidID int64
DeviceGroup []string
CheckinRule []string
BuildID string // aka. Build Fingerprint
Product string
Radio string
Bootloader string
SDKVersion int32
CellOperator string
CountryCode string
RawBatteryStats [][]string
RawPackageManager [][]string
}
// Counter is a wrapper for mapreduce counter. (e.g., mr.MapIO and mr.ReduceIO)
type Counter interface {
Count(name string, inc int)
}
// IntCounter implements Counter.
type IntCounter int
// Count increments the underlying int by inc.
func (c *IntCounter) Count(_ string, inc int) {
*c += IntCounter(inc)
}
// PrefixCounter is a wrapper that allows including a prefix to counted names.
type PrefixCounter struct {
Prefix string
Counter Counter
}
// Count increments the named counter by inc.
func (c *PrefixCounter) Count(name string, inc int) {
c.Counter.Count(c.Prefix+"-"+name, inc)
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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.
// Historian v2 analyzes bugreports and outputs battery analysis results.
package main
import (
"flag"
"fmt"
"log"
"net/http"
"github.com/google/battery-historian/analyzer"
)
var (
optimized = flag.Bool("optimized", true, "Whether to output optimized js files. Disable for local debugging.")
port = flag.Int("port", 9999, "service port")
)
type analysisServer struct{}
func (*analysisServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("Starting processing for: %s", r.Method)
analyzer.UploadHandler(w, r)
}
func initFrontend() {
http.HandleFunc("/", analyzer.UploadHandler)
http.Handle("/static/", http.FileServer(http.Dir(".")))
http.Handle("/compiled/", http.FileServer(http.Dir(".")))
if *optimized == false {
http.Handle("/third_party/", http.FileServer(http.Dir(".")))
http.Handle("/js/", http.FileServer(http.Dir(".")))
}
}
func main() {
flag.Parse()
initFrontend()
analyzer.InitTemplates()
analyzer.SetIsOptimized(*optimized)
log.Println("Listening on port: ", *port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 (
"flag"
"fmt"
"io/ioutil"
"log"
"time"
"github.com/golang/protobuf/proto"
bspb "github.com/google/battery-historian/pb/batterystats_proto"
)
var (
inputFile = flag.String("input", "", "bugreport to be read")
)
func min(x, y int) int {
if x < y {
return x
}
return y
}
func main() {
flag.Parse()
c, err := ioutil.ReadFile(*inputFile)
if err != nil {
log.Fatalf("Cannot open the file %s: %v", *inputFile, err)
}
br := string(c)
s := &bspb.Checkin{Checkin: proto.String(br)}
pkgs, errs := parse.ExtractAppsFromBugReport(br)
if len(errs) > 0 {
log.Fatalf("Errors encountered when getting package list: %v", errs)
}
var ctr parse.IntCounter
stats, warns, errs := parse.ParseBatteryStats(&ctr, parse.CreateCheckinReport(s), pkgs)
if len(warns) > 0 {
log.Printf("Encountered unexpected warnings: %v\n", warns)
}
if len(errs) > 0 {
log.Fatalf("Could not parse battery stats: %v\n", errs)
}
fmt.Println("\n################\n")
fmt.Println("Partial Wakelocks")
fmt.Println("################\n")
var pwl []*parse.WakelockInfo
for _, app := range stats.App {
for _, pw := range app.Wakelock {
if pw.GetPartialTimeMsec() > 0 {
pwl = append(pwl,
&parse.WakelockInfo{
Name: fmt.Sprintf("%s : %s", app.GetName(), pw.GetName()),
UID: app.GetUid(),
Duration: time.Duration(pw.GetPartialTimeMsec()) * time.Millisecond,
})
}
}
}
parse.SortByTime(pwl)
for _, pw := range pwl[:min(5, len(pwl))] {
fmt.Printf("%s (uid=%d) %s\n", pw.Duration, pw.UID, pw.Name)
}
fmt.Println("\n################")
fmt.Println("Kernel Wakelocks")
fmt.Println("################\n")
var kwl []*parse.WakelockInfo
for _, kw := range stats.System.KernelWakelock {
if kw.GetName() != "PowerManagerService.WakeLocks" && kw.GetTimeMsec() > 0 {
kwl = append(kwl, &parse.WakelockInfo{
Name: kw.GetName(),
Duration: time.Duration(kw.GetTimeMsec()) * time.Millisecond,
})
}
}
parse.SortByTime(kwl)
for _, kw := range kwl[:min(5, len(kwl))] {
fmt.Printf("%s %s\n", kw.Duration, kw.Name)
}
data, err := proto.Marshal(stats)
if err != nil {
log.Fatalf("Error from proto.Marshal: %v", err)
}
ioutil.WriteFile("checkin.proto", data, 0600)
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 (
"bufio"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"github.com/google/battery-historian/parseutils"
)
var (
summaryFormat = flag.String("summary", parseutils.FormatBatteryLevel, "1. batteryLevel 2. totalTime")
historyFile = flag.String("history", "", "Battery history file generated by `adb shell dumpsys batterystats -c --history-start <start>`")
csvFile = flag.String("csv", "", "Output filename to write csv timeseries data to.")
scrubPII = flag.Bool("scrub", true, "Whether ScrubPII is applied to addresses.")
)
func usage() {
fmt.Println("Incorrect summary argument. Format: --summary=[batteryLevel|totalTime] --history=<log-file> [--csv=<csv-output-file>]")
os.Exit(1)
}
func checkFlags() {
switch *summaryFormat {
case parseutils.FormatBatteryLevel:
case parseutils.FormatTotalTime:
default:
fmt.Println("1")
usage()
}
if *historyFile == "" {
fmt.Println("2")
usage()
}
}
func main() {
flag.Parse()
checkFlags()
// read the whole file
history, err := ioutil.ReadFile(*historyFile)
if err != nil {
log.Fatal(err)
}
writer := ioutil.Discard
if *csvFile != "" && *summaryFormat == parseutils.FormatTotalTime {
f, err := os.Create(*csvFile)
if err != nil {
log.Fatal(err)
}
defer f.Close()
csvWriter := bufio.NewWriter(f)
defer csvWriter.Flush()
writer = csvWriter
}
rep := parseutils.AnalyzeHistory(string(history), *summaryFormat, writer, *scrubPII)
// Exclude summaries with no change in battery level
var a []parseutils.ActivitySummary
for _, s := range rep.Summaries {
if s.InitialBatteryLevel != s.FinalBatteryLevel {
a = append(a, s)
}
}
if rep.TimestampsAltered {
fmt.Println("Some timestamps were changed while processing the log.")
}
if len(rep.Errs) > 0 {
fmt.Println("Errors encountered:")
for _, err := range rep.Errs {
fmt.Println(err.Error())
}
}
fmt.Println("\nNumber of summaries ", len(a), "\n")
for _, s := range a {
s.Print(&rep.OutputBuffer)
}
fmt.Println(rep.OutputBuffer.String())
}
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 csv contains functions to store EntryState events,
// and print them in CSV format.
package csv
import (
"fmt"
"io"
)
// FileHeader is outputted as the first line in csv files.
const FileHeader = "metric,type,start_time,end_time,value,opt"
// Entry contains the details of the start of a state.
type Entry struct {
Desc string
Start int64
Type string
Value string
// Additional data associated with the entry.
// Currently this is used to hold the UID (string) of a service (ServiceUID),
// and is an empty string for other types.
Opt string
}
// State holds the csv writer, and the map from metric key to active entry.
type State struct {
// For printing the CSV entries to.
writer io.Writer
entries map[Key]Entry
rebootEvent *Entry
}
// Key is the unique identifier for an entry.
type Key struct {
Metric, Identifier string
}
// NewState returns a new State.
func NewState(csvWriter io.Writer) *State {
// Write the csv header.
if csvWriter != nil {
fmt.Fprintln(csvWriter, FileHeader)
}
return &State{
writer: csvWriter,
entries: make(map[Key]Entry),
}
}
// HasRebootEvent returns true if a reboot event is currently stored, false otherwise.
func (s *State) HasRebootEvent() bool {
return (s.rebootEvent != nil)
}
// AddRebootEvent stores the entry for the reboot event,
// using the given curTime as the start time.
func (s *State) AddRebootEvent(curTime int64) {
s.rebootEvent = &Entry{
"Reboot",
curTime,
"bool",
"true",
"",
}
}
// PrintRebootEvent prints out the stored reboot event,
// using the given curTime as the end time.
func (s *State) PrintRebootEvent(curTime int64) {
if e := s.rebootEvent; e != nil {
s.print(e.Desc, e.Type, e.Start, curTime, e.Value, e.Opt)
s.rebootEvent = nil
}
}
// AddEntry adds the given entry into the existing map.
// If the entry already exists, it prints out the entry and deletes it.
func (s *State) AddEntry(desc string, newState EntryState, curTime int64) {
s.AddEntryWithOpt(desc, newState, curTime, "")
}
// AddEntryWithOpt adds the given entry into the existing map, with the optional
// value set.
// If the entry already exists, it prints out the entry and deletes it.
func (s *State) AddEntryWithOpt(desc string, newState EntryState, curTime int64, opt string) {
key := newState.GetKey(desc)
if e, ok := s.entries[key]; ok {
s.print(e.Desc, e.Type, e.Start, curTime, e.Value, e.Opt)
delete(s.entries, key)
return
}
if newState.GetStartTime() == 0 || newState.GetValue() == "" {
return
}
s.entries[key] = Entry{
desc,
curTime,
newState.GetType(),
newState.GetValue(),
opt,
}
}
func (s *State) print(desc, metricType string, start, end int64, value, opt string) {
if s.writer != nil {
fmt.Fprintf(s.writer, "%s,%s,%d,%d,%s,%s\n", desc, metricType, start, end, value, opt)
}
}
// PrintAllReset prints all active entries and resets the map.
func (s *State) PrintAllReset(curTime int64) {
for _, e := range s.entries {
s.print(e.Desc, e.Type, e.Start, curTime, e.Value, e.Opt)
}
s.entries = make(map[Key]Entry)
}
// EntryState is a commmon interface for the various types,
// so the Entries can access them the same way.
type EntryState interface {
// GetStartTime returns the start time of the entry.
GetStartTime() int64
// GetType returns the type of the entry:
// "string", "bool", "int" or "service".
GetType() string
// GetValue returns the stored value of the entry.
GetValue() string
// GetKey returns the unique identifier for the entry.
GetKey(string) Key
}
......@@ -52,16 +52,22 @@ getopt_sort_by_power = True
getopt_summarize_pct = -1
getopt_report_filename = ""
getopt_generate_chart_only = False
getopt_disable_chart_drawing = False
def usage():
"""Print usage of the script."""
print "\nUsage: %s [OPTIONS] [FILE]\n" % sys.argv[0]
print " -a: show all wakelocks (don't abbreviate system wakelocks)"
print " -c: disable drawing of chart"
print " -d: debug mode, output debugging info for this program"
print (" -e TIME: extend billing an extra TIME seconds after each\n"
" wakelock, or until the next wakelock is seen. Useful for\n"
" accounting for modem power overhead.")
print " -h: print this message."
print (" -m: generate output that can be embedded in an existing page.\n"
" HTML header and body tags are not outputted.")
print (" -n [CATEGORY=]PROC: output another row containing only processes\n"
" whose name matches uid of PROC in CATEGORY.\n"
" If CATEGORY is not specified, search in wake_lock_in.")
......@@ -136,6 +142,30 @@ def format_time(delta_time):
return timestr
def format_duration(dur_ms):
"""Return a time string representing the duration in human readable format."""
if not dur_ms:
return "0ms"
ms = dur_ms % 1000
dur_ms = (dur_ms - ms) / 1000
secs = dur_ms % 60
dur_ms = (dur_ms - secs) / 60
mins = dur_ms % 60
hrs = (dur_ms - mins) / 60
out = ""
if hrs > 0:
out += "%dh" % hrs
if mins > 0:
out += "%dm" % mins
if secs > 0:
out += "%ds" % secs
if ms > 0 or not out:
out += "%dms" % ms
return out
def get_event_category(e):
e = e.lstrip("+-")
earr = e.split("=")
......@@ -178,7 +208,7 @@ def get_event_subcat(cat, e):
is one of the categories tracked by concurrent_cat.
Default subcategory is the empty string.
"""
concurrent_cat = {"wake_lock_in", "sync", "top"}
concurrent_cat = {"wake_lock_in", "sync", "top", "job", "conn"}
if cat in concurrent_cat:
try:
return get_after_equal(e)
......@@ -252,7 +282,6 @@ def is_file_legacy_mode(input_file):
split_line = line.split()
if not split_line:
continue
line_time = split_line[0]
if "+" not in line_time and "-" not in line_time:
continue
......@@ -326,10 +355,12 @@ def parse_argv():
global getopt_sort_by_power, getopt_power_data_file
global getopt_summarize_pct, getopt_show_all_wakelocks
global getopt_report_filename
global getopt_generate_chart_only
global getopt_disable_chart_drawing
try:
opts, argv_rest = getopt.getopt(sys.argv[1:],
"ade:hn:p:q:r:s:tv", ["help"])
"acde:hmn:p:q:r:s:tv", ["help"])
except getopt.GetoptError as err:
print "<pre>\n"
print str(err)
......@@ -337,9 +368,11 @@ def parse_argv():
try:
for o, a in opts:
if o == "-a": getopt_show_all_wakelocks = True
if o == "-c": getopt_disable_chart_drawing = True
if o == "-d": getopt_debug = True
if o == "-e": getopt_bill_extra_secs = int(a)
if o in ("-h", "--help"): usage()
if o == "-m": getopt_generate_chart_only = True
if o == "-n": parse_search_option(a)
if o == "-p": getopt_power_data_file = a
if o == "-q": getopt_power_quanta = int(a)
......@@ -376,15 +409,20 @@ class Printer(object):
("running", "#990099"),
("wake_reason", "#b82e2e"),
("wake_lock_in", "#ff33cc"),
("job", "#cbb69d"),
("mobile_radio", "#aa0000"),
("data_conn", "#4070cf"),
("conn", "#ff6a19"),
("activepower", "#dd4477"),
("power", "#ff2222"),
("status", "#9ac658"),
("reboot", "#ddff77"),
("device_idle", "#37ff64"),
("motion", "#4070cf"),
("active", "#119fc8"),
("power_save", "#ff2222"),
("wifi", "#119fc8"),
("wifi_full_lock", "#888888"),
("wifi_scan", "#888888"),
("wifi_multicast", "#888888"),
("wifi_radio", "#888888"),
("wifi_running", "#109618"),
("wifi_suppl", "#119fc8"),
("wifi_signal_strength", "#9900aa"),
......@@ -392,17 +430,22 @@ class Printer(object):
("phone_scanning", "#dda0dd"),
("audio", "#990099"),
("phone_in_call", "#cbb69d"),
("wifi", "#119fc8"),
("bluetooth", "#cbb69d"),
("phone_state", "#dc3912"),
("signal_strength", "#119fc8"),
("video", "#cbb69d"),
("flashlight", "#cbb69d"),
("low_power", "#109618"),
("fg", "#dda0dd"),
("gps", "#ff9900"),
("reboot", "#ddff77"),
("power", "#ff2222"),
("status", "#9ac658"),
("health", "#888888"),
("plug", "#888888"),
("job", "#cbb69d")]
("charging", "#888888"),
("pkginst", "#cbb69d"),
("pkgunin", "#cbb69d")]
_ignore_categories = ["user", "userfg"]
......@@ -645,8 +688,9 @@ class BHEmitter(object):
_transitional_cats = ["plugged", "running", "wake_lock", "gps", "sensor",
"phone_in_call", "mobile_radio", "phone_scanning",
"proc", "fg", "top", "sync", "wifi", "wifi_full_lock",
"wifi_scan", "wifi_multicast", "wifi_running",
"bluetooth", "audio", "video", "wake_lock_in"]
"wifi_scan", "wifi_multicast", "wifi_running", "conn",
"bluetooth", "audio", "video", "wake_lock_in", "job",
"device_idle", "wifi_radio"]
_in_progress_dict = autovivify() # events that are currently in progress
_proc_dict = {} # mapping of "proc" uid to human-readable name
_search_proc_id = -1 # proc id of the getopt_proc_name
......@@ -1073,6 +1117,7 @@ class PowerEmitter(object):
for i in report_list:
print i[1]
print "total: %.3f mAh, %d events" % (total_mah, total_count)
print "</pre>\n"
def adjust_reboot_time(line, event_time):
......@@ -1086,7 +1131,76 @@ def adjust_reboot_time(line, event_time):
return wall_time - event_time, wall_time
def get_app_id(uid):
"""Returns the app ID from a string.
Reverses and uses the methods defined in UserHandle.java to get
only the app ID.
Args:
uid: a string representing the uid printed in the history output
Returns:
An integer representing the specific app ID.
"""
abr_uid_re = re.compile(r"u(?P<userId>\d+)(?P<aidType>[ias])(?P<appId>\d+)")
if not uid:
return 0
if uid.isdigit():
# 100000 is the range of uids allocated for a user.
return int(uid) % 100000
if abr_uid_re.match(uid):
match = abr_uid_re.search(uid)
try:
d = match.groupdict()
if d["aidType"] == "i": # first isolated uid
return int(d["appId"]) + 99000
if d["aidType"] == "a": # first application uid
return int(d["appId"]) + 10000
return int(d["appId"]) # app id wasn't modified
except IndexError:
sys.stderr.write("Abbreviated app UID didn't match properly")
return uid
app_cpu_usage = {}
def save_app_cpu_usage(uid, cpu_time):
uid = get_app_id(uid)
if uid in app_cpu_usage:
app_cpu_usage[uid] += cpu_time
else:
app_cpu_usage[uid] = cpu_time
# Constants defined in android.net.ConnectivityManager
conn_constants = {
"0": "TYPE_MOBILE",
"1": "TYPE_WIFI",
"2": "TYPE_MOBILE_MMS",
"3": "TYPE_MOBILE_SUPL",
"4": "TYPE_MOBILE_DUN",
"5": "TYPE_MOBILE_HIPRI",
"6": "TYPE_WIMAX",
"7": "TYPE_BLUETOOTH",
"8": "TYPE_DUMMY",
"9": "TYPE_ETHERNET",
"17": "TYPE_VPN",
}
def main():
details_re = re.compile(r"^Details:\scpu=\d+u\+\d+s\s*(\((?P<appCpu>.*)\))?")
app_cpu_usage_re = re.compile(
r"(?P<uid>\S+)=(?P<userTime>\d+)u\+(?P<sysTime>\d+)s")
proc_stat_re = re.compile((r"^/proc/stat=(?P<usrTime>-?\d+)\s+usr,\s+"
r"(?P<sysTime>-?\d+)\s+sys,\s+"
r"(?P<ioTime>-?\d+)\s+io,\s+"
r"(?P<irqTime>-?\d+)\s+irq,\s+"
r"(?P<sirqTime>-?\d+)\s+sirq,\s+"
r"(?P<idleTime>-?\d+)\s+idle.*")
)
data_start_time = 0.0
data_stop_time = 0
data_stop_timestr = ""
......@@ -1102,10 +1216,17 @@ def main():
highlight_dict = {} # search result for -n option
is_first_data_line = True
is_dumpsys_format = False
argv_remainder = parse_argv()
input_file = argv_remainder[0]
legacy_mode = is_file_legacy_mode(input_file)
proc_stat_summary = {
"usr": 0,
"sys": 0,
"io": 0,
"irq": 0,
"sirq": 0,
"idle": 0,
}
if legacy_mode:
input_string = LegacyFormatConverter().convert(input_file)
......@@ -1125,6 +1246,8 @@ def main():
if line.isspace(): break
line = line.strip()
if "RESET:TIME: " in line:
data_start_time = parse_reset_time(line)
continue
......@@ -1141,6 +1264,43 @@ def main():
p = re.compile('"[^"]+"')
line = p.sub(space_escape, line)
if details_re.match(line):
match = details_re.search(line)
try:
d = match.groupdict()
if d["appCpu"]:
for app in d["appCpu"].split(", "):
app_match = app_cpu_usage_re.search(app)
try:
a = app_match.groupdict()
save_app_cpu_usage(a["uid"],
int(a["userTime"]) + int(a["sysTime"]))
except IndexError:
sys.stderr.write("App CPU usage line didn't match properly")
except IndexError:
sys.stderr.write("Details line didn't match properly")
continue
elif proc_stat_re.match(line):
match = proc_stat_re.search(line)
try:
d = match.groupdict()
if d["usrTime"]:
proc_stat_summary["usr"] += int(d["usrTime"])
if d["sysTime"]:
proc_stat_summary["sys"] += int(d["sysTime"])
if d["ioTime"]:
proc_stat_summary["io"] += int(d["ioTime"])
if d["irqTime"]:
proc_stat_summary["irq"] += int(d["irqTime"])
if d["sirqTime"]:
proc_stat_summary["sirq"] += int(d["sirqTime"])
if d["idleTime"]:
proc_stat_summary["idle"] += int(d["idleTime"])
except IndexError:
sys.stderr.write("proc/stat line didn't match properly")
continue
# pull apart input line by spaces
split_line = line.split()
if len(split_line) < 4: continue
......@@ -1184,7 +1344,21 @@ def main():
bhemitter.handle_event(event_time, format_time(time_delta_s),
"battery_level=" + line_battery_level,
emit_dict, time_dict, highlight_dict)
for event in line_events:
# conn events need to be parsed in order to be useful
if event.startswith("conn"):
num, ev = get_after_equal(event).split(":")
if ev == "\"CONNECTED\"":
event = "+conn="
else:
event = "-conn="
if num in conn_constants:
event += conn_constants[num]
else:
event += "UNKNOWN"
bhemitter.handle_event(event_time, format_time(time_delta_s), event,
emit_dict, time_dict, highlight_dict)
......@@ -1217,7 +1391,8 @@ def main():
printer = Printer()
print "<!DOCTYPE html>\n<html><head>\n"
if not getopt_generate_chart_only:
print "<!DOCTYPE html>\n<html><head>\n"
report_filename = argv_remainder[0]
if getopt_report_filename:
report_filename = getopt_report_filename
......@@ -1233,14 +1408,18 @@ def main():
print("<p><b>WARNING:</b> legacy format detected; "
"history information is limited</p>\n")
print """
<script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization',
'version':'1','packages':['timeline']}]}"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript">
if not getopt_generate_chart_only:
print """
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript" src="https://www.google.com/jsapi?autoload={'modules':[{'name':'visualization','version':'1','packages':['timeline']}]}"></script>
"""
print "<script type=\"text/javascript\">"
google.setOnLoadCallback(drawChart);
if not getopt_disable_chart_drawing:
print "google.setOnLoadCallback(drawChart);\n"
print """
var dataTable;
var chart;
var options;
......@@ -1270,7 +1449,7 @@ function drawChart() {
chart.draw(dataTable, options);
//get vertical coordinate of scale bar
var svg = document.getElementsByTagName('svg')[0];
var svg = document.getElementById('chart').getElementsByTagName('svg')[0];
var label = svg.children[2].children[0];
var y = label.getAttribute('y');
//plus height of scale bar
......@@ -1283,6 +1462,8 @@ function drawChart() {
svg.setAttribute('height', chart_height);
var content = $('#chart').children()[0];
$(content).css('height', chart_height);
var inner = $(content).children()[0];
$(inner).css('height', chart_height);
}
......@@ -1299,16 +1480,21 @@ function redrawChart() {
width:100px;
}
</style>
</head>
<body>
"""
if not getopt_generate_chart_only:
print "</head>\n<body>\n"
show_complete_time = False
if data_stop_time - data_start_time > 24 * 60 * 60:
show_complete_time = True
start_localtime = time_float_to_human(data_start_time, show_complete_time)
stop_localtime = time_float_to_human(data_stop_time, show_complete_time)
print ('<div id="chart"><b>WARNING: Visualizer disabled. '
'If you see this message, download the HTML then open it.</b></div>')
print "<div id=\"chart\">"
if not getopt_generate_chart_only:
print ("<b>WARNING: Visualizer disabled. "
"If you see this message, download the HTML then open it.</b>")
print "</div>"
print("<p><b>WARNING:</b>\n"
"<br>*: wake_lock field only shows the first/last wakelock held \n"
"when the system is awake. For more detail, use wake_lock_in."
......@@ -1345,10 +1531,35 @@ width:100px;
power_emitter.report()
if app_cpu_usage:
print "<b>App CPU usage:</b>"
print "<table border=\"1\"><tr><td>UID</td><td>Duration</td></tr>"
for (uid, use) in sorted(app_cpu_usage.items(), key=lambda x: -x[1]):
print "<tr><td>%s</td>" % uid
print "<td>%s</td></tr>" % format_duration(use)
print "</table>"
print "<br /><b>Proc/stat summary</b><ul>"
print "<li>Total User Time: %s</li>" % format_duration(
proc_stat_summary["usr"])
print "<li>Total System Time: %s</li>" % format_duration(
proc_stat_summary["sys"])
print "<li>Total IO Time: %s</li>" % format_duration(
proc_stat_summary["io"])
print "<li>Total Irq Time: %s</li>" % format_duration(
proc_stat_summary["irq"])
print "<li>Total Soft Irq Time: %s</li>" % format_duration(
proc_stat_summary["sirq"])
print "<li>Total Idle Time: %s</li>" % format_duration(
proc_stat_summary["idle"])
print "</ul>"
print "<pre>Process table:"
print bhemitter.procs_to_str()
print "</pre>\n"
print "</body>\n</html>"
if not getopt_generate_chart_only:
print "</body>\n</html>"
if __name__ == "__main__":
......
/**
*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
goog.provide('historian.Bars');
goog.require('goog.math.Range');
goog.require('historian.Context');
goog.require('historian.color');
goog.require('historian.time');
goog.require('historian.util');
/**
* Bars creates the individual lines for each data point of each series,
* as well as the tooltip displayed when a bar is hovered over.
* Each series is rendered in a horizontal line.
*
* @param {!historian.Context} context The visualisation context.
* @param {!historian.SeriesData} barData The array of series to display.
* @param {!historian.util.ServiceMapper} serviceMapper
* The map from service to uid.
* @constructor
*/
historian.Bars = function(context, barData, serviceMapper) {
/** @private {!historian.Context} */
this.context_ = context;
/** @private {!historian.SeriesData} */
this.barData_ = barData;
/** @private {!historian.util.ServiceMapper} */
this.serviceMapper_ = serviceMapper;
/**
* Tooltip for displaying data point information on mouse hover.
* @private {!historian.Bars.Tooltip_}
*/
this.tooltip_ = new historian.Bars.Tooltip_();
var clustered = this.cluster(this.getVisibleData_());
// Create a svg group for each series.
var series = this.context_.vis
.append('g')
.attr('class', 'bars-group')
.selectAll('.series')
.data(clustered)
.enter()
.append('g')
.attr('class', function(d) {
return 'series ' + d.name;
});
this.drawSeries_(series);
// Horizontal dividers for each series.
this.context_.seriesLinesGroup
.selectAll('.series-line')
.data(barData)
.enter()
.append('line')
.attr('x1', 0)
.attr('y1', function(d) {
return historian.Bars.getY(d.index) -
historian.Bars.DIVIDER_OFFSET_PX_ + historian.Context.margins.TOP;
})
.attr('x2', historian.Context.VIS_WIDTH + historian.Context.margins.LEFT)
.attr('y2', function(d) {
return (historian.Bars.getY(d.index) -
historian.Bars.DIVIDER_OFFSET_PX_ + historian.Context.margins.TOP);
})
.attr('stroke', 'lightgray');
// Text labels for each of the series.
this.context_.svg.append('g')
.selectAll('.series-label')
.data(barData)
.enter()
.append('text')
.attr('class', 'vis-label')
.attr('x', historian.Context.margins.LEFT -
historian.Bars.LABEL_OFFSET_PX_)
.attr('y', function(d) {
return ((historian.Bars.getY(d.index) +
historian.Context.margins.TOP +
historian.Bars.TEXT_OFFSET_PX_));
})
.text(function(d) {
if (d.name == 'wakelock_in') {
return 'Partial wakelock';
}
return d.name;
});
this.addAppSelectorListener_();
this.context_.registerRedraw(this);
};
/**
* Horizontal dividers between series are rendered slightly above the series.
* @const @private {number}
*/
historian.Bars.DIVIDER_OFFSET_PX_ = 2;
/** @const @private {number} */
historian.Bars.SERIES_OFFSET_PX_ = 12;
/** @const @private {number} */
historian.Bars.TEXT_OFFSET_PX_ = historian.Bars.SERIES_OFFSET_PX_ + 5;
/** @const @private {number} */
historian.Bars.LABEL_OFFSET_PX_ = 10;
/**
* The minimum px width for each bar line.
* @const @private {number}
*/
historian.Bars.MIN_BAR_WIDTH_PX_ = 2;
/**
* Adds the event listeners for the app selector.
* @private
*/
historian.Bars.prototype.addAppSelectorListener_ = function() {
var appSelector = document.getElementById('appSelector');
if (appSelector) {
appSelector.addEventListener('change', this.displaySelectedApp_.bind(this));
}
var clearApp = document.getElementById('clearApp');
if (clearApp) {
clearApp.addEventListener('click', this.displaySelectedApp_.bind(this));
}
};
/**
* Creates lines for each of the data points in each series.
* @param {!historian.SeriesData} series The array of series to render.
* @private
*/
historian.Bars.prototype.drawSeries_ = function(series) {
var self = this;
// For each series, draw a bar line for each data point.
series.each(function(parentDatum) {
d3.select(this).selectAll('line')
.data(function(d) {
return d.values;
})
.enter()
.append('line')
.attr('class', parentDatum.name + ' line')
.attr('x1',
function(d) { return self.context_.xScale(d.start_time); }.bind(this))
.attr('x2', self.drawAdjustedEndTime_.bind(self))
.attr('y1', function(d) {
return historian.Bars.getY(parentDatum.index) +
historian.Bars.SERIES_OFFSET_PX_;
})
.attr('y2', function(d) {
return historian.Bars.getY(parentDatum.index) +
historian.Bars.SERIES_OFFSET_PX_;
})
.on('mouseover', self.onMouseover_.bind(self, parentDatum))
.on('mouseout', self.onMouseout_.bind(self))
.style('stroke', function(d) {
// Use count to determine color for aggregated stats.
if (historian.util.isAggregatedMetric(parentDatum.name)) {
return parentDatum.color(d.clustered_count);
}
return parentDatum.color(d.getMaxValue());
});
});
};
/**
* Redraws all the bar lines for the current zoom level.
*/
historian.Bars.prototype.redraw = function() {
this.updateSeries_();
// Readjusts all series for panning / zooming.
this.context_.vis.selectAll('.line')
.attr('x1', function(d) {
return this.context_.xScale(d.start_time);
}.bind(this))
.attr('x2', this.drawAdjustedEndTime_.bind(this));
};
/**
* Updates the data binded to bar elements and redraws all the changed series.
* @private
*/
historian.Bars.prototype.updateSeries_ = function() {
var uid = null;
var e = document.getElementById('appSelector');
if (e) {
var v = e.options[e.selectedIndex].value;
if (v != 'none_chosen') {
uid = v;
}
}
var filteredData = [];
this.barData_.forEach(function(series) {
var values = /** @type {!Array<!historian.Entry>} */
(series.values.filter(this.inViewableRange.bind(this)));
if (uid && (series.name in historian.util.appSpecificMetrics)) {
values = this.filterServices(values, uid);
}
filteredData.push({
'name': series.name,
'type': series.type,
'color': series.color,
'index': series.index,
'values': values
});
}, this);
var clustered = this.cluster(filteredData);
var series = this.context_.vis
.select('.bars-group')
.selectAll('.series')
.data(clustered, function(d) {
return d.name + d.values;
});
series.exit()
.remove();
var newSeries = series.enter()
.append('g')
.attr('class', function(d) {
return 'series ' + d.name;
});
// Draws all the changed series.
this.drawSeries_(newSeries);
};
/**
* Update the app specific metrics of the graph.
* @private
*/
historian.Bars.prototype.displaySelectedApp_ = function() {
this.updateSeries_();
};
/**
* Removes services not present in servicesMatcher
* @param {!Array<!historian.Entry>} data The data to filter.
* @param {number} uid The uid to match.
* @return {!Array<!historian.AggregatedEntry>} matching data.
*/
historian.Bars.prototype.filterServices = function(data, uid) {
var matching = [];
data.forEach(function(d) {
var values = [];
var services = [];
if (d.services != null) {
services = d.services;
} else {
services.push(d.value);
}
services.forEach(function(s) {
if (this.serviceMapper_.uid(s) == uid) {
values.push(s);
}
}, this);
if (values.length > 0) {
matching.push({
'start_time': d.start_time,
'end_time': d.end_time,
'value': d.value,
'services': values
});
}
}, this);
return matching;
};
/**
* Changes the cluster's end time to the start time added to the
* active duration of the cluster.
* If this is less than the visible duration for the current
* zoom level, it is set to the minimum visible.
* @param {!historian.util.ClusterEntry} d The data point to display.
* @return {number} value to plot.
* @private
*/
historian.Bars.prototype.drawAdjustedEndTime_ = function(d) {
var msPerPixel = this.msPerPixel_();
var adjustedEndTime = d.start_time + d.active_duration;
// Check if the duration of the event is long enough that it would
// reach the minimum px width when rendered.
var minDuration = historian.Bars.MIN_BAR_WIDTH_PX_ * msPerPixel;
if (d.active_duration < minDuration) {
adjustedEndTime = d.start_time + minDuration;
}
return this.context_.xScale(adjustedEndTime);
};
/**
* Returns only the data that occurs in the currently visible time range.
* @return {!historian.SeriesData} The matching data.
* @private
*/
historian.Bars.prototype.getVisibleData_ = function() {
var result = [];
this.barData_.forEach(function(series) {
result.push({
'name': series.name,
'type': series.type,
'color': series.color,
'index': series.index,
'values': series.values.filter(this.inViewableRange.bind(this))
});
}, this);
return result;
};
/**
* Returns whether a data point is visible in the current time range.
* @param {(!historian.Entry|!historian.AggregatedEntry)} v The data point.
* @return {boolean} true if visible, false otherwise.
*/
historian.Bars.prototype.inViewableRange = function(v) {
var startTime = this.context_.xScale.invert(0);
var endTime = this.context_.xScale.invert(historian.Context.VIS_WIDTH);
var dataRange = new goog.math.Range(v['start_time'], v['end_time']);
var extent = new goog.math.Range(startTime, endTime);
return goog.math.Range.hasIntersection(dataRange, extent);
};
/**
* Returns the bar data clustered, based on the current zoom level.
* @param {!historian.SeriesData} data The data to cluster.
* @return {!Array<!historian.ClusteredSerieData>} Clustered data.
*/
historian.Bars.prototype.cluster = function(data) {
var msPerPixel = this.msPerPixel_();
var clustered = historian.util.cluster(data, msPerPixel);
return clustered;
};
/**
* Returns the duration visible for 1 pixel.
* @return {number}
* @private
*/
historian.Bars.prototype.msPerPixel_ = function() {
var startTime = this.context_.xScale.invert(0);
var endTime = this.context_.xScale.invert(historian.Context.VIS_WIDTH);
var ext = endTime - startTime;
return Math.round(Number(ext / historian.Context.VIS_WIDTH));
};
/**
* Displays a tooltip for the bar being hovered over.
* @param {!historian.ClusteredSerieData} parentDatum
* The series data for the datum being hovered over.
* @param {!historian.util.ClusterEntry} d The datum point being hovered on.
* @private
*/
historian.Bars.prototype.onMouseover_ = function(parentDatum, d) {
var formattedLines = [
historian.time.getDate(d.start_time),
historian.time.getTime(d.start_time) + ' - ' +
historian.time.getTime(d.end_time)
];
formattedLines.push('<b>active duration:</b> ' +
historian.time.formatDuration(d.active_duration));
var name = parentDatum.name;
if (parentDatum.name == 'Partial wakelock') {
name = 'First wakelock acquired';
} else if (parentDatum.name == 'wakelock_in') {
name = 'Partial wakelock';
}
formattedLines.push('<b>' + name + '</b>' + ': ' +
d.clustered_count + ' occurences');
// Boolean entries don't have associated values other than true.
// Don't display values for wakelocks as it's only the
// first wakelock acquired.
if (parentDatum.type != 'bool') {
formattedLines.push('');
// Display the values in order of duration.
var sorted = d.getSortedValues();
sorted.forEach(function(s) {
var value =
historian.color.valueFormatter(parentDatum.name, s['value']);
var duration = historian.time.formatDuration(s['duration']);
var count = s['count'] + ' count';
formattedLines.push(value + ': ' + duration + ', ' + count);
});
}
this.tooltip_.update_(d3.event.pageX, d3.event.pageY, formattedLines);
};
/**
* Hides the tooltip from view.
* @private
*/
historian.Bars.prototype.onMouseout_ = function() {
this.tooltip_.hide_();
};
/**
* Returns the y coordinate the series line should be rendered on.
* @param {number} index The index of the series. The highest numbered series
* is rendered at the top of the graph.
* @return {number} The y coordinate corresponding to the index.
*/
historian.Bars.getY = function(index) {
return ((historian.Context.VIS_HEIGHT / 20) * (20 - index - 1));
};
/**
* Class for displaying a tooltip when hovering over a bar line.
* @constructor
* @private
*/
historian.Bars.Tooltip_ = function() {
/** @private {!d3.selection} */
this.tooltip_ = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', historian.Bars.Tooltip_.MIN_VISIBILITY);
};
/** @const {number} */
historian.Bars.Tooltip_.MAX_VISIBILITY = 0.9;
/** @const {number} */
historian.Bars.Tooltip_.MIN_VISIBILITY = 0;
/** @const {number} */
historian.Bars.Tooltip_.TRANSITION_MS = 300;
/** @const {number} */
historian.Bars.Tooltip_.LEFT_OFFSET_PX = 5;
/** @const {number} */
historian.Bars.Tooltip_.LETTER_WIDTH_PX = 8;
/** @const {number} */
historian.Bars.Tooltip_.TEXT_HEIGHT_PX = 20;
/** @const {number} */
historian.Bars.Tooltip_.MAX_WIDTH_PX = 600;
/**
* Updates the contents, position, size, and visibility of the tooltip.
* @param {number} x The x coordinate of the event.
* @param {number} y The y coordinate of the event.
* @param {!Array.<string>} lines The Html contents to display in tooltip.
* @private
*/
historian.Bars.Tooltip_.prototype.update_ = function(x, y, lines) {
this.tooltip_.html(lines.join('<br>'))
.style('left', + (x + historian.Bars.Tooltip_.LEFT_OFFSET_PX) + 'px')
.style('top', + y + 'px');
// Find the length of the longest line, so we can set the width of the box.
var maxLength = 0;
lines.forEach(function(line) {
if (line.length > maxLength) {
maxLength = line.length;
}
});
var width = maxLength * historian.Bars.Tooltip_.LETTER_WIDTH_PX;
// Increase height of tooltip display for each extra line.
var height = lines.length * historian.Bars.Tooltip_.TEXT_HEIGHT_PX;
this.tooltip_
.style('width', width + 'px')
.style('height', height + 'px');
this.setVisibility_(historian.Bars.Tooltip_.MAX_VISIBILITY);
};
/**
* Sets the opacity of the tooltip.
* @param {number} v The visibility value.
* @private
*/
historian.Bars.Tooltip_.prototype.setVisibility_ = function(v) {
this.tooltip_.transition()
.duration(historian.Bars.Tooltip_.TRANSITION_MS)
.style('opacity', v);
};
/**
* Hides the tooltip from view.
* @private
*/
historian.Bars.Tooltip_.prototype.hide_ = function() {
this.setVisibility_(historian.Bars.Tooltip_.MIN_VISIBILITY);
};
/**
*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
/**
* @fileoverview Contains color configurations and generateSeriesColors for
* creating color functions for each series.
*/
goog.provide('historian.color');
goog.require('goog.functions');
/**
* Map from series name to color function.
* @private {Object}
*/
historian.color.colorMap_ = {};
historian.color.colorMap_['Brightness'] = d3.scale.ordinal()
.domain([0, 1, 2, 3, 4])
.range(['white', '#addeed', '#415094', '#2c2782', '#060424']);
historian.color.colorMap_['Phone scanning'] =
goog.functions.constant('#251dcc');
historian.color.colorMap_['Network connectivity'] = d3.scale.ordinal()
.domain(['TYPE_MOBILE', 'TYPE_WIFI', 'TYPE_OTHER'])
.range(['#8ac1ff', '#ffd48a', '#ff9a8a', '#ff749b', '#99ff74']);
historian.color.colorMap_['Data connection'] = d3.scale.ordinal()
.domain(['none', 'edge', 'hsdpa', 'hspa', 'lte', 'hspap'])
.range(['white', '#63ff52', '#ff5263', '#ff9d52', '#527aff', 'black']);
historian.color.colorMap_['Phone state'] = d3.scale.ordinal()
.domain(['in', 'out'])
.range(['#F0FFFF', 'red']);
historian.color.colorMap_['Mobile radio'] = d3.scale.ordinal()
.domain(['true'])
.range(['#fa531b']);
historian.color.colorMap_['CPU running'] = d3.scale.ordinal()
.domain(['true'])
.range(['black']);
historian.color.colorMap_['Screen'] = d3.scale.ordinal()
.domain(['true'])
.range(['red']);
historian.color.colorMap_['Signal strength'] = d3.scale.ordinal()
.domain([0, 1, 2, 3, 4])
.range(['white', 'red', 'orange', 'yellow', 'green']);
historian.color.colorMap_['Foreground process'] = function() {
var scale = d3.scale.ordinal()
.domain([0, 1, 2, 3, 4, 5, 6, 7])
.range(['white', '#b7ff73', '#73b7ff', '#ff73b9',
'#ffa973', '#ff9673', '#ffef73', '#73faff']);
return function(value) {
if (value > 7) {
return scale(7);
}
return scale(value);
};
}();
historian.color.colorMap_['SyncManager app'] = function() {
var scale = d3.scale.ordinal()
.domain([0, 1, 2])
.range(['white', '#ff6816', '#16a2ff']);
return function(value) {
if (value > 2) {
return scale(2);
}
return scale(value);
};
}();
historian.color.colorMap_['Partial wakelock'] =
goog.functions.constant('#A4D3EE');
historian.color.colorMap_['wakelock_in'] = function() {
var scale = d3.scale.ordinal()
.domain([0, 1, 2, 3, 4, 5])
.range(['white', 'orange', 'red', 'green', 'blue', 'black']);
return function(value) {
if (value > 5) {
return scale(5);
}
return scale(value);
};
}();
/**
* Maps a value into a string to display.
* @private
*/
historian.color.valueTextMap_ = {};
historian.color.valueTextMap_['Brightness'] = {
0: 'off',
1: 'dark',
2: 'dim',
3: 'moderate',
4: 'bright'
};
historian.color.valueTextMap_['Signal strength'] = {
0: 'none',
1: 'poor',
2: 'moderate',
3: 'good',
4: 'great'
};
historian.color.valueTextMap_['Charging status'] = {
'c': 'charging',
'd': 'discharging'
};
/**
* Returns the formatted string for the value if defined in the valueTextMap_
* above, otherwise returns the original value.
*
* @param {string} metric Name of metric.
* @param {(string | number)} v Value to format.
* @return {(string | number)} Formatted output.
*/
historian.color.valueFormatter = function(metric, v) {
if (metric in historian.color.valueTextMap_) {
return historian.color.valueTextMap_[metric][v];
}
return v;
};
/**
* Sets the color function for each series.
* This is either from the config file, or a linear scale if none exists.
*
* @param {!historian.SeriesData} seriesData Array of series to set
* color functions for.
*/
historian.color.generateSeriesColors = function(seriesData) {
var color = d3.scale.category20c();
seriesData.forEach(function(s) {
// Predefined color functions from config file.
if (s['name'] in historian.color.colorMap_) {
s['color'] = historian.color.colorMap_[s['name']];
// Create a different color for each string name.
} else if (s.type === 'string' || s.type === 'service') {
s['color'] = d3.scale.category20c();
// Bool series only need one color (no entries for 0 values).
} else if (s.type === 'bool') {
var seriesColor = color(s['name']);
s['color'] = function(c) {
return seriesColor;
};
// Create a linear color scale.
} else {
var extent = d3.extent(s.values, function(d) {
return d.value;
});
s['color'] = d3.scale.linear()
.domain([extent[0], extent[1]])
.range(['#FFFFFF', color(s['name'])]);
}
});
};
/**
*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
goog.provide('historian.Context');
/**
* Class containing the outer svg elements, axes, and scales.
* Manages zoom events, calling redraw on registered objects.
*
* @param {!Array.<number>} xExtent Min and max start_time value of the data.
* @param {number} numSeries The number of series to display as bars.
* This is to adjust the height of the svg if necessary.
* @constructor
*/
historian.Context = function(xExtent, numSeries) {
// Add a margin on either side of the graph.
var graphSize =
historian.Context.VIS_WIDTH - (2 * historian.Context.VIS_MARGIN_);
var msPerPixel = Math.round((xExtent[1] - xExtent[0]) / graphSize);
var marginSize = msPerPixel * historian.Context.VIS_MARGIN_;
/** @type {function(number): number} */
this.xScale = d3.time.scale()
.domain([xExtent[0] - marginSize, xExtent[1] + marginSize])
.range([0, historian.Context.VIS_WIDTH]);
/** @type {function(number): number} */
this.yScale = d3.scale.linear()
.domain([0, 100])
.range([historian.Context.VIS_HEIGHT, 0]);
/** @private {Object} */
this.xAxis_ = d3.svg.axis()
.scale(this.xScale);
/** @private {Object} */
this.yAxis_ = d3.svg.axis()
.scale(this.yScale)
.orient('right');
if (numSeries > 20) {
var addedHeight = ((numSeries - 20) * (historian.Context.VIS_HEIGHT / 20));
historian.Context.margins.TOP += addedHeight;
historian.Context.SVG_HEIGHT += addedHeight;
}
/**
* The outer svg element.
* @type {!Object}
*/
this.svg = d3.select('#historian-graph')
.append('svg')
.attr('width', historian.Context.SVG_WIDTH)
.attr('height', historian.Context.SVG_HEIGHT);
// The series lines are rendered later on in bars.js, however we want
// the lines to appear below everything else.
this.seriesLinesGroup = this.svg.append('g');
// Create clip path for restricting region of chart.
var clip = this.svg.append('svg:clipPath')
.attr('id', 'clip')
.append('svg:rect')
.attr('x', 0)
.attr('y', 0 - historian.Context.margins.TOP)
.attr('width', historian.Context.VIS_WIDTH)
.attr('height', historian.Context.SVG_HEIGHT);
/**
* The main chart area.
* @type {!Object}
*/
this.vis = this.svg.append('g')
.attr('transform',
'translate(' + historian.Context.margins.LEFT +
',' + historian.Context.margins.TOP + ')')
.attr('clip-path', 'url(#clip)');
// Add axes.
this.vis.append('svg:g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, ' + historian.Context.VIS_HEIGHT + ')')
.call(this.xAxis_);
var yAxisXOffset =
historian.Context.margins.LEFT + historian.Context.VIS_WIDTH;
this.svg.append('svg:g')
.attr('class', 'y axis')
.attr('transform',
'translate(' + yAxisXOffset +
', ' + historian.Context.margins.TOP + ')')
.call(this.yAxis_);
/**
* For storing objects that need to be redrawn on zoom.
* @type {!Array.<Object>}
* @private
*/
this.zoomObjects_ = [];
this.zoom = d3.behavior.zoom()
.x(this.xScale)
.scaleExtent([1, 512])
.on('zoom', this.redraw_.bind(this));
this.svg.call(this.zoom);
};
/**
* Margins between svg and visualisation.
*/
historian.Context.margins = {
TOP: 120,
RIGHT: 100,
BOTTOM: 50,
LEFT: 150
};
/**
* Margin to show on either side of the graph in the zoomed out view.
* @private
*/
historian.Context.VIS_MARGIN_ = 50;
/** @private {string} */
historian.Context.WINDOW_WIDTH_ = window.getComputedStyle(
document.getElementsByTagName('body')[0], null).
getPropertyValue('width');
/** @type {number} */
historian.Context.SVG_WIDTH = parseFloat(historian.Context.WINDOW_WIDTH_) - 50;
/** @type {number} */
historian.Context.SVG_HEIGHT = 800;
/** @const {number} */
historian.Context.VIS_WIDTH =
historian.Context.SVG_WIDTH -
historian.Context.margins.LEFT -
historian.Context.margins.RIGHT;
/** @type {number} */
historian.Context.VIS_HEIGHT =
historian.Context.SVG_HEIGHT -
historian.Context.margins.TOP -
historian.Context.margins.BOTTOM;
/** @const @private {number} */
historian.Context.VERTICAL_SCROLL_ = 0;
/** @const @private {number} */
historian.Context.HORIZONTAL_SCROLL_ = 1;
/**
* Saves a reference to object that will have redraw called on zoom.
* @param {!Object} o The object to save.
*/
historian.Context.prototype.registerRedraw = function(o) {
this.zoomObjects_.push(o);
};
/**
* Extra px allowed panning before the start and after the end of the graph.
* @private {number}
*/
historian.Context.PAN_MARGIN_PX_ = 100;
/**
* Rerenders the graph for the current zoom level.
* Calls all registered objects to redraw themselves.
* @private
*/
historian.Context.prototype.redraw_ = function() {
var translateX = this.zoom.translate()[0];
var translateY = this.zoom.translate()[1];
// Don't let the user pan too far right. Any positive value means
// we're showing white space on the left.
translateX = Math.min(historian.Context.PAN_MARGIN_PX_, translateX);
// Limit panning to the left.
var zoomedWidth = this.zoom.scale() * historian.Context.VIS_WIDTH;
var limitedPan =
historian.Context.VIS_WIDTH - zoomedWidth -
historian.Context.PAN_MARGIN_PX_;
translateX = Math.max(limitedPan, translateX);
this.zoom.translate([translateX, translateY]);
var scrollType = historian.Context.VERTICAL_SCROLL_;
var sourceEvent = d3.event.sourceEvent;
if (sourceEvent.type == 'wheel') {
var x = sourceEvent.wheelDeltaX;
var y = sourceEvent.wheelDeltaY;
if (x != 0) {
if (Math.abs(x) > Math.abs(y)) {
// Assume trying to scroll horizontally.
scrollType = historian.Context.HORIZONTAL_SCROLL_;
}
}
}
if (scrollType == historian.Context.HORIZONTAL_SCROLL_) {
// Horizontal scrolling over graph doesn't do anything,
// scroll the page instead.
window.scrollBy(-sourceEvent.wheelDeltaX * 0.1, 0);
} else {
this.svg.select('.x.axis').call(this.xAxis_);
this.zoomObjects_.forEach(function(o) {
o.redraw();
});
}
};
/**
*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
goog.provide('historian.Historian');
goog.require('historian.Bars');
goog.require('historian.Context');
goog.require('historian.LevelLine');
goog.require('historian.util');
/**
* A single data point for a serie.
*
* @typedef {{
* start_time: number,
* end_time: number,
* value: (string | number)
* }}
*/
historian.Entry;
/**
* A single data point for a aggregated serie.
*
* @typedef {{
* start_time: number,
* end_time: number,
* value: (string | number),
* services: !Array<string>
* }}
*/
historian.AggregatedEntry;
/**
* The data for a single serie.
*
* @typedef {{
* name: string,
* type: string,
* values: Array<(!historian.Entry|!historian.AggregatedEntry)>,
* index: number,
* color: (function(string): string | undefined)
* }}
*/
historian.SerieData;
/**
* The clustered data for a single serie.
*
* @typedef {{
* name: string,
* type: string,
* values: Array<(!historian.util.ClusterEntry)>,
* index: number,
* color: (function(string): string | undefined)
* }}
*/
historian.ClusteredSerieData;
/**
* The data for all the series.
*
* @typedef {!Array<!historian.SerieData>}
*/
historian.SeriesData;
/**
* The object for the level line, bar data, graph extent and service mappings.
*
* @typedef {{
* barData: !historian.SeriesData,
* levelData: !historian.SerieData,
* extent: !Array<number>,
* serviceMapper: !historian.util.ServiceMapper
* }}
*/
historian.AllData;
/**
* Creates the historian graph from the csv data.
* @export
*/
historian.render = function() {
var historianCsv = d3.select('#csv-data')
.text();
var data = historian.util.readCsv( /** @type {string} */ (historianCsv));
var context = new historian.Context(data.extent, data.barData.length);
var bars = new historian.Bars(context, data.barData, data.serviceMapper);
var levelLine = new historian.LevelLine(context, data.levelData);
};
/**
*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
goog.provide('historian.LevelLine');
goog.require('historian.Context');
goog.require('historian.time');
/**
* LevelLine creates the battery level chart line, and the vertical
* hover line and text showing battery level information.
*
* @param {!historian.Context} context The visualisation context.
* @param {!historian.SerieData} levelData The battery level series to display.
* @constructor
*/
historian.LevelLine = function(context, levelData) {
/** @private {function(number): number} */
this.xScale_ = context.xScale;
/** @private {!historian.SerieData} */
this.levelData_ = levelData;
this.adjustLevelData_();
/** @private {function(Object)} */
this.levelLine_ = d3.svg.line()
.x(function(d) {
return context.xScale(d.start_time);
}.bind(this))
.y(function(d) {
return context.yScale(d.value);
}.bind(this))
.interpolate('linear');
/**
* Battery level line from the data.
* @private {!d3.selection}
*/
this.line_ = context.vis.append('svg:path')
.attr('class', 'battery level')
.attr('d', this.levelLine_(this.levelData_))
.attr('stroke', historian.LevelLine.COLOR_)
.attr('stroke-width', 4)
.attr('fill', 'none');
/**
* Vertical hover line that is shown above the battery level line.
* @private {!Object}
*/
this.hoverLine_ = context.svg.append('line')
.attr('class', 'hover')
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', 0)
.attr('y2', historian.Context.SVG_HEIGHT)
.attr('stroke', 'black');
/**
* Text information and line highlighter for battery level.
* @private {!historian.LevelLine.LevelDisplay_}
*/
this.levelDisplay_ =
new historian.LevelLine.LevelDisplay_(context, this.levelData_);
this.createLegend_(context);
context.svg.on('mousemove', this.onMousemove_.bind(this));
context.registerRedraw(this);
};
/**
* Color of the battery level line.
* @private {string}
*/
historian.LevelLine.COLOR_ = 'blue';
/**
* Size of legend box.
* @private {number}
*/
historian.LevelLine.LEGEND_SIZE_PX_ = 15;
/**
* Label to display in the legend box.
* @private {string}
*/
historian.LevelLine.LEGEND_LABEL_ = 'Battery Level';
/**
* Redraws the battery level line. Called on zoom.
*/
historian.LevelLine.prototype.redraw = function() {
this.line_
.attr('d', this.levelLine_(this.levelData_));
this.levelDisplay_.redraw_();
};
/**
* Creates an extra entry using the end time of the last entry.
*
* Each battery level entry has a start and end time. Since we only use
* the start time as data points in the line graph, we need to create
* an extra point for the end time for the very last entry.
* @private
*/
historian.LevelLine.prototype.adjustLevelData_ = function() {
if (this.levelData_.length > 0) {
var last = this.levelData_[this.levelData_.length - 1];
var newEntry = {
start_time: last.end_time,
end_time: last.end_time,
value: last.value
};
this.levelData_.push(newEntry);
}
};
/**
* Renders the legend for the battery level line.
* @param {!historian.Context} context The visualization context.
* @private
*/
historian.LevelLine.prototype.createLegend_ = function(context) {
var startX =
historian.Context.SVG_WIDTH - historian.Context.margins.RIGHT + 65;
var startY = historian.Context.SVG_HEIGHT / 2;
var legend = context.svg.append('g')
.attr('class', 'level-legend')
.attr('x', startX)
.attr('y', startY);
legend.append('rect')
.attr('x', startX)
.attr('y', startY)
.attr('width', historian.LevelLine.LEGEND_SIZE_PX_)
.attr('height', historian.LevelLine.LEGEND_SIZE_PX_)
.style('fill', historian.LevelLine.COLOR_);
var textCoordY = startY + (historian.LevelLine.LEGEND_SIZE_PX_ * 2);
legend.append('text')
.attr('x', startX)
.attr('y', textCoordY)
.attr('transform', 'rotate(90 ' + startX + ',' + textCoordY + ')')
.text(historian.LevelLine.LEGEND_LABEL_);
};
/**
* Updates the hover line and battery level text on mouse move.
* @private
*/
historian.LevelLine.prototype.onMousemove_ = function() {
// Get coordinates relative to SVG rather than page.
var coords = d3.mouse(d3.select('#historian-graph').node());
// Position the hoverLine x coordinate to be on the mouse coordinates.
this.hoverLine_
.attr('x1', + (coords[0]) + 'px')
.attr('x2', + (coords[0]) + 'px')
.style('opacity', 1);
// Get the time value of the chart corresponding to the mouse position.
var xValue =
this.xScale_.invert(coords[0] - historian.Context.margins.LEFT);
// Get the index of the data point corresponding to the time value.
// The bisector finds where the time value bisects the data array.
var bisector = d3.bisector(function(d) {
return d.start_time;
}).right;
var i = bisector(this.levelData_, xValue) - 1;
if (i < this.levelData_.length - 1 &&
xValue >= this.levelData_[0].start_time) {
this.levelDisplay_.update_(i, coords, xValue);
} else {
// Time does not match data point - mouse is too far left
// or right of chart. Hide battery level text and line highlighter.
this.levelDisplay_.hide_();
}
};
/**
* Class for displaying information about the current battery level
* the mouse is hovering over, as well as the line highlighter.
*
* @param {!historian.Context} context The visualisation context.
* @param {!historian.SerieData} levelData The battery level series to display.
* @constructor
* @private
*/
historian.LevelLine.LevelDisplay_ = function(context, levelData) {
/** @private {!historian.SerieData} */
this.levelData_ = levelData;
/** @private {function(number): number} */
this.xScale_ = context.xScale;
/** @private {function(number): number} */
this.yScale_ = context.yScale;
/** @private {?historian.Entry} */
this.levelStart_ = null;
/** @private {?historian.Entry} */
this.levelEnd_ = null;
/**
* Colored line for overlaying on top of battery level line,
* for the level currently under mouse over.
* @private {!Object}
*/
this.levelHighlighter_ = context.vis
.append('line')
.attr('class', 'battery level highlighter')
.attr('stroke', 'darkorange')
.attr('stroke-width', 4);
/**
* textGroup allows for multi line svg text elements.
* @private {!Object}
*/
this.textGroup_ = context.svg.append('text')
.attr('x', 10)
.attr('y', 0)
.style('fill', '#000000')
.style('stroke', 'none')
.style('font-size', '18px');
/** @private {Array.<Object>} */
this.lines_ = [];
this.addLines_(historian.LevelLine.LevelDisplay_.NUM_LINES);
};
/** @const {number} */
historian.LevelLine.LevelDisplay_.MAX_VISIBILITY = 1;
/** @const {number} */
historian.LevelLine.LevelDisplay_.MIN_VISIBILITY = 0;
/** @const {number} */
historian.LevelLine.LevelDisplay_.NUM_LINES = 4;
/** @const {number} */
historian.LevelLine.LevelDisplay_.PADDING_PX = 10;
/**
* Creates the tspan elements.
* @param {number} numLines Number of tspan elements to create.
* @private
*/
historian.LevelLine.LevelDisplay_.prototype.addLines_ = function(numLines) {
for (var i = 0; i < numLines; i++) {
var line = this.textGroup_.append('tspan')
.attr('x', 0)
.attr('dy', 20);
this.lines_.push(line);
}
};
/**
* Sets the contents of the tspan element of the index with the given line.
* @param {number} i The index of tspan element.
* @param {string} line The contents to set tspan to.
* @private
*/
historian.LevelLine.LevelDisplay_.prototype.setLine_ = function(i, line) {
this.lines_[i].text(line);
};
/**
* Updates the battery level display for the given data index.
* @param {number} i Index of the data point of the start of the duration
* to display,
* @param {Object} coords The coordinate of the mouse event.
* @param {number} time The time value corresponding to the mouse position.
* @private
*/
historian.LevelLine.LevelDisplay_.prototype.update_ =
function(i, coords, time) {
// Set text display to be right of mouse cursor, at top of page.
var x = coords[0] + historian.LevelLine.LevelDisplay_.PADDING_PX;
this.textGroup_.attr('transform', 'translate(' + x + ', 20)');
// Start and end level data points corresponding to index.
this.levelStart_ = this.levelData_[i];
this.levelEnd_ = this.levelData_[i + 1];
// Calculate details for text display.
var timeText = 'Current time: ' + historian.time.getTime(time);
var batteryLevelText =
'Battery level: between ' + this.levelStart_.value +
' and ' + this.levelEnd_.value;
var dischargeText =
historian.LevelLine.calculateDischarge_(this.levelStart_, this.levelEnd_);
var duration = historian.time.formatDuration(
this.levelEnd_.start_time - this.levelStart_.start_time);
var durationText = 'Duration: ' + duration +
', from ' + historian.time.getTime(this.levelStart_.start_time) +
' to ' + historian.time.getTime(this.levelEnd_.start_time);
// Set contents of text display.
this.setLine_(0, timeText);
this.setLine_(1, batteryLevelText);
this.setLine_(2, dischargeText);
this.setLine_(3, durationText);
this.redraw_();
this.setVisibility_(historian.LevelLine.LevelDisplay_.MAX_VISIBILITY);
};
/**
* Redraws all the battery level display for the currnet zoom level.
* @private
*/
historian.LevelLine.LevelDisplay_.prototype.redraw_ = function() {
if (this.levelStart_ != null && this.levelEnd_ != null) {
var xStart = this.xScale_(this.levelStart_.start_time);
var yStart = this.yScale_(/** @type {number} */ (this.levelStart_.value));
var xEnd = this.xScale_(this.levelEnd_.start_time);
var yEnd = this.yScale_(/** @type {number} */ (this.levelEnd_.value));
// Highlight section of battery level line currently being mouse overed.
this.levelHighlighter_
.attr('x1', xStart)
.attr('y1', yStart)
.attr('x2', xEnd)
.attr('y2', yEnd);
}
};
/**
* Sets the opacity of the level display elements.
* @param {number} opacity The opacity to set elements to.
* @private
*/
historian.LevelLine.LevelDisplay_.prototype.setVisibility_ = function(opacity) {
this.textGroup_.style('fill-opacity', opacity);
this.levelHighlighter_.style('stroke-opacity', opacity);
};
/**
* Hides the level display from view.
* @private
*/
historian.LevelLine.LevelDisplay_.prototype.hide_ = function() {
this.setVisibility_(historian.LevelLine.LevelDisplay_.MIN_VISIBILITY);
};
/**
* Static function for calculating the % discharge / charge rate / hour,
* given 2 battery level data entries, returning it in string format.
*
* @param {!historian.Entry} d1 The data point for start.
* @param {!historian.Entry} d2 The data point for end.
* @return {string} Returns the formatted discharge / charge rate information.
* @private
*/
historian.LevelLine.calculateDischarge_ = function(d1, d2) {
var levelDifference = d2.value - d1.value;
var timeDiff = d2.start_time - d1.start_time;
var rate = (historian.time.MSECS_IN_SEC * historian.time.SECS_IN_MIN *
historian.time.MINS_IN_HOUR) /
timeDiff * levelDifference;
// Round rate to 2 decimal points.
var formatted = parseFloat(Math.abs(rate)).toFixed(2) + ' % / hour';
if (rate > 0) {
formatted = 'Charge rate: ' + formatted;
} else {
formatted = 'Discharge rate: ' + formatted;
}
return formatted;
};
/**
*
* Copyright 2015 Google Inc. All Rights Reserved.
*
* 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.
*/
/**
* @fileoverview Time related helper functions to format time
* into readable formats, and calculate durations.
*/
goog.provide('historian.time');
/** @const {number} */
historian.time.MSECS_IN_SEC = 1000;
/** @const {number} */
historian.time.SECS_IN_MIN = 60;
/** @const {number} */
historian.time.MINS_IN_HOUR = 60;
/**
* Returns the date formatted in "Month Day Year".
* @param {number} t The Unix timestamp to format.
* @return {string} The formatted date "Month Day Year".
*/
historian.time.getDate = function(t) {
var d = new Date(t);
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
return months[d.getMonth()] + ' ' + d.getDate() + ' ' + d.getFullYear();
};
/**
* Returns the time formatted in 'hh:mm:ss'.
* @export
* @param {number} t The Unix timestamp to format.
* @return {string} The formatted time 'hh:mm:ss'.
*/
historian.time.getTime = function(t) {
var d = new Date(t);
return (
historian.time.padTime_(d.getHours()) + ':' +
historian.time.padTime_(d.getMinutes()) + ':' +
historian.time.padTime_(d.getSeconds()));
};
/**
* Pads the unit to two digits by prepending a 0 if the length is 1.
* @param {number} u The number to format.
* @return {string} The formatted number as two digits in a string.
* @private
*/
historian.time.padTime_ = function(u) {
if ((u + '').length === 1) {
return '0' + u;
}
return '' + u;
};
/**
* Returns the ms duration formatted as a human readable string.
* Format is "1h 3m 4s 30ms".
* @export
* @param {number} duration The time duration in ms.
* @return {string} The formatted duration.
*/
historian.time.formatDuration = function(duration) {
var ms = duration % historian.time.MSECS_IN_SEC;
var s = Math.floor(
(duration / historian.time.MSECS_IN_SEC) % historian.time.SECS_IN_MIN);
var m = Math.floor((duration /
(historian.time.MSECS_IN_SEC * historian.time.SECS_IN_MIN)) %
historian.time.MINS_IN_HOUR);
var h = Math.floor((duration / (historian.time.MSECS_IN_SEC *
historian.time.SECS_IN_MIN * historian.time.MINS_IN_HOUR)));
var formatted = '';
if (h > 0) {
formatted += h + 'h ';
}
if (m > 0 || h > 0) {
formatted += m + 'm ';
}
if (s > 0 || m > 0 || h > 0) {
formatted += s + 's ';
}
if (ms > 0 || formatted.length === 0) {
// Some of the ms would have been converted from microseconds and would
// therefore have fractional components. Only show decimals if there is
// a fractional component.
if (Math.round(ms) !== ms) {
ms = ms.toFixed(2);
}
formatted += ms + 'ms';
}
return formatted.trim();
};
此差异已折叠。
此差异已折叠。
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 packageutils
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/golang/protobuf/proto"
"github.com/google/battery-historian/parseutils"
usagepb "github.com/google/battery-historian/pb/usagestats_proto"
)
// Time format that firstInstallTime and lastUpdateTime are in, using the constants defined in the Golang time package
const timeFormat = "2006-01-02 15:04:05"
var (
// serviceDumpRE is a regular expression to match the beginning of a service dump.
serviceDumpRE = regexp.MustCompile(`^DUMP\s+OF\s+SERVICE\s+(?P<service>\S+):`)
// uidRE is a regular expression to match a uid line in the appops section (ie 'Uid 1000:').
uidRE = regexp.MustCompile(`^Uid\s+(?P<uid>\S+):`)
// userIDRE is a regular expression to match a userId line in the package dump section (ie 'userId=1000 gids=[3003]').
userIDRE = regexp.MustCompile(`^userId=(?P<uid>\S+)\s+gids.*`)
// appOpsPackageRE is a regular expression to match a package line in the appops dump section (ie. 'Package android:')
appOpsPackageRE = regexp.MustCompile(`Package\s+(?P<package>\S+):`)
// packageDumpPackageRE is a regular expression to match a package line in the package dump section (ie. 'Package android:')
packageDumpPackageRE = regexp.MustCompile(`Package\s+\[(?P<package>\S+)\]\s+\(.*`)
// packageDumpVersionCodeRE is a regular expression to match a version code line in the package dump section (ie. 'versionCode=94 targetSdk=19')
packageDumpVersionCodeRE = regexp.MustCompile(`versionCode=(?P<versionCode>\d+)\stargetSdk=\d+`)
// packageDumpVersionNameRE is a regular expression to match a version name line in the package dump section (ie. 'versionName=4.0.3')
packageDumpVersionNameRE = regexp.MustCompile(`versionName=(?P<versionName>\S+)`)
// firstInstallTimeRE is a regular expression to match the firstInstallTime line in the package dump section (ie. 'firstInstallTime=2014-12-05 14:23:12')
firstInstallTimeRE = regexp.MustCompile("firstInstallTime=(?P<time>.*)")
// lastUpdateTimeRE is a regular expression to match the lastUpdateTime line in the package dump section (ie. 'lastUpdateTime=2014-12-05 18:23:12')
lastUpdateTimeRE = regexp.MustCompile("lastUpdateTime=(?P<time>.*)")
)
// extractAppsFromAppOpsDump looks at the app ops service dump from a bug report
// and extracts package names and their UIDs from the dump. It returns a mapping of
// the package name to the PackageInfo object.
func extractAppsFromAppOpsDump(s string) (map[string]*usagepb.PackageInfo, []error) {
pkgs := make(map[string]*usagepb.PackageInfo)
var errs []error
inAppOpsSection := false
var curUID int32
var err error
Loop:
for _, line := range strings.Split(s, "\n") {
line = strings.TrimSpace(line)
if m, result := parseutils.SubexpNames(serviceDumpRE, line); m {
switch in := result["service"] == "appops"; {
case inAppOpsSection && !in: // Just exited the App Ops section
break Loop
case in:
inAppOpsSection = true
continue Loop
default: // Random section
continue Loop
}
}
if !inAppOpsSection {
continue
}
if m, result := parseutils.SubexpNames(uidRE, line); m {
curUID, err = AppIDFromString(result["uid"])
if err != nil {
errs = append(errs, err)
}
}
if m, result := parseutils.SubexpNames(appOpsPackageRE, line); m {
pkg := result["package"]
pkgs[pkg] = &usagepb.PackageInfo{
PkgName: proto.String(pkg),
Uid: proto.Int32(curUID),
}
}
}
return pkgs, errs
}
// extractAppsFromPackageDump looks at the package service dump from a bug report
// and extracts as much application info from the dump. It returns a mapping of
// the package name to the PackageInfo object.
func extractAppsFromPackageDump(s string) (map[string]*usagepb.PackageInfo, []error) {
pkgs := make(map[string]*usagepb.PackageInfo)
var errs []error
var inPackageDumpSection, inCurrentSection bool
var curPkg *usagepb.PackageInfo
Loop:
for _, line := range strings.Split(s, "\n") {
line = strings.TrimSpace(line)
if m, result := parseutils.SubexpNames(serviceDumpRE, line); m {
switch in := result["service"] == "package"; {
case inPackageDumpSection && !in: // Just exited package dump section
break Loop
case in:
inPackageDumpSection = true
continue Loop
default: // Random section
continue Loop
}
}
if !inPackageDumpSection {
continue
}
switch line {
case "Packages:":
inCurrentSection = true
continue Loop
case "Hidden system packages:":
inCurrentSection = false
break Loop
}
if !inCurrentSection {
continue
}
if m, result := parseutils.SubexpNames(packageDumpPackageRE, line); m {
if curPkg != nil {
pkgs[curPkg.GetPkgName()] = curPkg
}
curPkg = &usagepb.PackageInfo{
PkgName: proto.String(result["package"]),
}
} else if m, result := parseutils.SubexpNames(userIDRE, line); m {
if curPkg == nil {
errs = append(errs, errors.New("found userId line before package line"))
continue
}
uid, err := AppIDFromString(result["uid"])
if err != nil {
errs = append(errs, err)
}
curPkg.Uid = proto.Int32(uid)
} else if m, result := parseutils.SubexpNames(packageDumpVersionCodeRE, line); m {
if curPkg == nil {
errs = append(errs, errors.New("found versionCode line before package line"))
continue
}
vc, err := strconv.Atoi(result["versionCode"])
if err != nil {
errs = append(errs, fmt.Errorf("error getting version code from string: %v\n", err))
continue
}
curPkg.VersionCode = proto.Int32(int32(vc))
} else if m, result := parseutils.SubexpNames(packageDumpVersionNameRE, line); m {
if curPkg == nil {
errs = append(errs, errors.New("found versionName line before package line"))
continue
}
curPkg.VersionName = proto.String(result["versionName"])
} else if m, result := parseutils.SubexpNames(firstInstallTimeRE, line); m {
if curPkg == nil {
errs = append(errs, errors.New("found firstInstallTime line before package line"))
continue
}
t, err := time.Parse(timeFormat, result["time"])
if err != nil {
errs = append(errs, err)
}
curPkg.FirstInstallTime = proto.Int64(t.UnixNano() / int64(time.Millisecond))
} else if m, result := parseutils.SubexpNames(lastUpdateTimeRE, line); m {
if curPkg == nil {
errs = append(errs, errors.New("found lastUpdateTime line before package line"))
continue
}
t, err := time.Parse(timeFormat, result["time"])
if err != nil {
errs = append(errs, err)
}
curPkg.LastUpdateTime = proto.Int64(t.UnixNano() / int64(time.Millisecond))
}
}
if curPkg != nil {
pkgs[curPkg.GetPkgName()] = curPkg
}
return pkgs, errs
}
// ExtractAppsFromBugReport looks through a bug report and extracts as much application info
// as possible.
func ExtractAppsFromBugReport(s string) ([]*usagepb.PackageInfo, []error) {
var pkgs []*usagepb.PackageInfo
pdPkgs, pdErrs := extractAppsFromPackageDump(s)
aoPkgs, aoErrs := extractAppsFromAppOpsDump(s)
errs := append(aoErrs, pdErrs...)
for name, pdPkg := range pdPkgs {
// Favor info from package dump since we'll have more data from there.
pkgs = append(pkgs, pdPkg)
// Remove related data from appops dump to avoid listing a package twice.
delete(aoPkgs, name)
}
for _, aoPkg := range aoPkgs {
pkgs = append(pkgs, aoPkg)
}
return pkgs, errs
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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.
syntax = "proto2";
package build;
message Build {
// Fingerprint, e.g. "google/mysid/toro:4.0.4/IMM06/243892:userdebug/dev-keys"
optional string fingerprint = 1;
// Carrier, e.g. "google"
optional string brand = 2;
// Product name, e.g. "mysid"
optional string product = 3;
// Product name, e.g. "toro"
optional string device = 4;
// Release version, e.g. "4.0.4"
optional string release = 5;
// Build id, e.g. "IMM06"
optional string build_id = 6;
// Incremental build id, e.g. "243892"
optional string incremental = 7;
// Type of build, e.g. "userdebug"
optional string type = 8;
// Tags, e.g. "dev-keys"
repeated string tags = 9;
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册