whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
.git
|
||||
.localstorage
|
||||
node_modules
|
||||
devtools
|
||||
.eslint*
|
||||
.gitignore
|
||||
.prettier*
|
||||
.stylelint*
|
||||
CONTRIBUTING.md
|
||||
LICENSE
|
||||
npm
|
||||
npx
|
||||
package-lock.json
|
||||
package.json
|
||||
PATENTS
|
||||
README.md
|
||||
tsconfig.json
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"root": true,
|
||||
"ignorePatterns": ["*.min.js"]
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# Treat all files in the repo as binary, with no git magic updating
|
||||
# line endings. This produces predictable results in different environments.
|
||||
#
|
||||
# Windows users contributing to Go will need to use a modern version
|
||||
# of git and editors capable of LF line endings.
|
||||
#
|
||||
# Windows .bat files are known to have multiple bugs when run with LF
|
||||
# endings. So if they are checked in with CRLF endings, there should
|
||||
# be a test like the one in test/winbatch.go in the go repository.
|
||||
# (See golang.org/issue/37791.)
|
||||
#
|
||||
# See golang.org/issue/9281.
|
||||
|
||||
* -text
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
.localstorage
|
||||
@@ -0,0 +1 @@
|
||||
{"proseWrap": "always"}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": ["stylelint-config-standard"],
|
||||
"rules": {
|
||||
"declaration-property-value-allowed-list": {
|
||||
"/color/": ["/^var\\(--/", "transparent"]
|
||||
},
|
||||
"unit-disallowed-list": ["px"],
|
||||
"selector-class-pattern": "^[a-zA-Z\\-]+$"
|
||||
},
|
||||
"ignoreFiles": ["**/*.min.css"]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# Contributing to Go
|
||||
|
||||
Go is an open source project.
|
||||
|
||||
It is the work of hundreds of contributors. We appreciate your help!
|
||||
|
||||
## Filing issues
|
||||
|
||||
When [filing an issue](https://golang.org/issue/new), make sure to answer these
|
||||
five questions:
|
||||
|
||||
1. What version of Go are you using (`go version`)?
|
||||
2. What operating system and processor architecture are you using?
|
||||
3. What did you do?
|
||||
4. What did you expect to see?
|
||||
5. What did you see instead?
|
||||
|
||||
General questions should go to the
|
||||
[golang-nuts mailing list](https://groups.google.com/group/golang-nuts) instead
|
||||
of the issue tracker. The gophers there will answer or ask you to file an issue
|
||||
if you've tripped over a bug.
|
||||
|
||||
## Contributing code
|
||||
|
||||
Please read the
|
||||
[Contribution Guidelines](https://golang.org/doc/contribute.html) before sending
|
||||
patches.
|
||||
|
||||
Unless otherwise noted, the Go source files are distributed under the BSD-style
|
||||
license found in the LICENSE file.
|
||||
@@ -0,0 +1,27 @@
|
||||
Copyright 2009 The Go Authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,22 @@
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Google as part of the Go project.
|
||||
|
||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||
patent license to make, have made, use, offer to sell, sell, import,
|
||||
transfer and otherwise run, modify and propagate the contents of this
|
||||
implementation of Go, where such license applies only to those patent
|
||||
claims, both currently owned or controlled by Google and acquired in
|
||||
the future, licensable by Google that are necessarily infringed by this
|
||||
implementation of Go. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute or
|
||||
order or agree to the institution of patent litigation against any
|
||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that this implementation of Go or any code incorporated within this
|
||||
implementation of Go constitutes direct or contributory patent
|
||||
infringement, or inducement of patent infringement, then any patent
|
||||
rights granted to you under this License for this implementation of Go
|
||||
shall terminate as of the date such litigation is filed.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Go Telemetry
|
||||
|
||||
This repository holds the Go Telemetry server code and libraries, used for
|
||||
hosting [telemetry.go.dev](https://telemetry.go.dev) and instrumenting Go
|
||||
toolchain programs with opt-in telemetry.
|
||||
|
||||
**Warning**: this repository is intended for use only in tools maintained by
|
||||
the Go team, including tools in the Go distribution and auxiliary tools like
|
||||
[gopls](https://pkg.go.dev/golang.org/x/tools/gopls) or
|
||||
[govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck). There are
|
||||
no compatibility guarantees for any of the packages here: public APIs will
|
||||
change in breaking ways as the telemetry integration is refined.
|
||||
|
||||
## Notable Packages
|
||||
|
||||
- The [x/telemetry/counter](https://pkg.go.dev/golang.org/x/telemetry/counter)
|
||||
package provides a library for instrumenting programs with counters and stack
|
||||
reports.
|
||||
- The [x/telemetry/upload](https://pkg.go.dev/golang.org/x/telemetry/upload)
|
||||
package provides a hook for Go toolchain programs to upload telemetry data,
|
||||
if the user has opted in to telemetry uploading.
|
||||
- The [x/telemetry/cmd/gotelemetry](https://pkg.go.dev/pkg/golang.org/x/telemetry/cmd/gotelemetry)
|
||||
command is used for managing telemetry data and configuration.
|
||||
- The [x/telemetry/config](https://pkg.go.dev/pkg/golang.org/x/telemetry/config)
|
||||
package defines the subset of telemetry data that has been approved for
|
||||
uploading by the telemetry proposal process.
|
||||
- The [x/telemetry/godev](https://pkg.go.dev/pkg/golang.org/x/telemetry/godev) directory defines
|
||||
the services running at [telemetry.go.dev](https://telemetry.go.dev).
|
||||
|
||||
## Contributing
|
||||
|
||||
This repository uses Gerrit for code changes. To learn how to submit changes to
|
||||
this repository, see https://golang.org/doc/contribute.html.
|
||||
|
||||
The main issue tracker for the time repository is located at
|
||||
https://github.com/golang/go/issues. Prefix your issue with "x/telemetry:" in
|
||||
the subject line, so it is easy to find.
|
||||
|
||||
### Linting & Formatting
|
||||
|
||||
This repository uses [eslint](https://eslint.org/) to format TS files,
|
||||
[stylelint](https://stylelint.io/) to format CSS files, and
|
||||
[prettier](https://prettier.io/) to format TS, CSS, Markdown, and YAML files.
|
||||
|
||||
See the style guides:
|
||||
|
||||
- [TypeScript](https://google.github.io/styleguide/tsguide.html)
|
||||
- [CSS](https://go.dev/wiki/CSSStyleGuide)
|
||||
|
||||
It is encouraged that all TS and CSS code be run through formatters before
|
||||
submitting a change. However, it is not a strict requirement enforced by CI.
|
||||
|
||||
### Installing npm Dependencies:
|
||||
|
||||
1. Install [docker](https://docs.docker.com/get-docker/)
|
||||
2. Run `./npm install`
|
||||
|
||||
### Run ESLint, Stylelint, & Prettier
|
||||
|
||||
./npm run all
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by TestDocHelp; DO NOT EDIT.
|
||||
|
||||
// Gotelemetry is a tool for managing Go telemetry data and settings.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// gotelemetry <command> [arguments]
|
||||
//
|
||||
// The commands are:
|
||||
//
|
||||
// on enable telemetry collection and uploading
|
||||
// local enable telemetry collection but disable uploading
|
||||
// off disable telemetry collection and uploading
|
||||
// view run a web viewer for local telemetry data
|
||||
// env print the current telemetry environment
|
||||
// clean remove all local telemetry data
|
||||
//
|
||||
// Use "gotelemetry help <command>" for details about any command.
|
||||
//
|
||||
// The following additional commands are available for diagnostic
|
||||
// purposes, and may change or be removed in the future:
|
||||
//
|
||||
// csv print all known counters
|
||||
// dump view counter file data
|
||||
// upload run upload with logging enabled
|
||||
package main
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/internal/testenv"
|
||||
)
|
||||
|
||||
var updateDocs = flag.Bool("update", false, "if set, update docs")
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv("GOTELEMETRY_RUN_AS_MAIN") != "" {
|
||||
main()
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestDocHelp(t *testing.T) {
|
||||
testenv.MustHaveExec(t)
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(exe, "help")
|
||||
cmd.Env = append(os.Environ(), "GOTELEMETRY_RUN_AS_MAIN=1")
|
||||
help, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if *updateDocs {
|
||||
var lines []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(help)), "\n") {
|
||||
if len(line) > 0 {
|
||||
lines = append(lines, "// "+line)
|
||||
} else {
|
||||
lines = append(lines, "//")
|
||||
}
|
||||
}
|
||||
contents := fmt.Sprintf(`// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by TestDocHelp; DO NOT EDIT.
|
||||
|
||||
%s
|
||||
package main
|
||||
`, strings.Join(lines, "\n"))
|
||||
|
||||
data, err := format.Source([]byte(contents))
|
||||
if err != nil {
|
||||
t.Fatalf("formatting content: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("doc.go", data, 0666); err != nil {
|
||||
t.Fatalf("writing doc.go: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := parser.ParseFile(token.NewFileSet(), "doc.go", nil, parser.PackageClauseOnly|parser.ParseComments)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing doc.go: %v", err)
|
||||
}
|
||||
doc := f.Doc.Text()
|
||||
if got, want := doc, string(help); got != want {
|
||||
t.Errorf("doc.go: mismatching content\ngot:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
// Copyright 2016 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package browser provides utilities for interacting with users' browsers.
|
||||
// This is a copy of the go project's src/cmd/internal/browser.
|
||||
package browser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Commands returns a list of possible commands to use to open a url.
|
||||
func Commands() [][]string {
|
||||
var cmds [][]string
|
||||
if exe := os.Getenv("BROWSER"); exe != "" {
|
||||
cmds = append(cmds, []string{exe})
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
cmds = append(cmds, []string{"/usr/bin/open"})
|
||||
case "windows":
|
||||
cmds = append(cmds, []string{"cmd", "/c", "start"})
|
||||
default:
|
||||
if os.Getenv("DISPLAY") != "" {
|
||||
// xdg-open is only for use in a desktop environment.
|
||||
cmds = append(cmds, []string{"xdg-open"})
|
||||
}
|
||||
}
|
||||
cmds = append(cmds,
|
||||
[]string{"chrome"},
|
||||
[]string{"google-chrome"},
|
||||
[]string{"chromium"},
|
||||
[]string{"firefox"},
|
||||
)
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Open tries to open url in a browser and reports whether it succeeded.
|
||||
func Open(url string) bool {
|
||||
for _, args := range Commands() {
|
||||
cmd := exec.Command(args[0], append(args[1:], url)...)
|
||||
if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// appearsSuccessful reports whether the command appears to have run successfully.
|
||||
// If the command runs longer than the timeout, it's deemed successful.
|
||||
// If the command runs within the timeout, it's deemed successful if it exited cleanly.
|
||||
func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool {
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
errc <- cmd.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
return true
|
||||
case err := <-errc:
|
||||
return err == nil
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// csv dumps all the active counters. The output is
|
||||
// a sequence of lines
|
||||
// value,"counter-name",program, version,go-version,goos, garch
|
||||
// sorted by counter name. It looks at the files in
|
||||
// telemetry.LocalDir that are counter files or local reports
|
||||
// By design it pays no attention to dates. The combination
|
||||
// of program version and go version are deemed sufficient.
|
||||
package csv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/telemetry/internal/counter"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
path, name string
|
||||
// one of counters or report is set
|
||||
counters *counter.File
|
||||
report *telemetry.Report
|
||||
}
|
||||
|
||||
func Csv() {
|
||||
files, err := readdir(telemetry.Default.LocalDir(), nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f.name, "v1.count") {
|
||||
buf, err := os.ReadFile(f.path)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
continue
|
||||
}
|
||||
cf, err := counter.Parse(f.name, buf)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
continue
|
||||
}
|
||||
f.counters = cf
|
||||
} else if strings.HasSuffix(f.name, ".json") {
|
||||
buf, err := os.ReadFile(f.path)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
continue
|
||||
}
|
||||
var x telemetry.Report
|
||||
if err := json.Unmarshal(buf, &x); err != nil {
|
||||
log.Print(err)
|
||||
continue
|
||||
}
|
||||
f.report = &x
|
||||
}
|
||||
}
|
||||
printTable(files)
|
||||
}
|
||||
|
||||
type record struct {
|
||||
goos, garch, program, version, goversion string
|
||||
cntr string
|
||||
count int
|
||||
}
|
||||
|
||||
func printTable(files []*file) {
|
||||
lines := make(map[string]*record)
|
||||
work := func(k string, v int64, rec *record) {
|
||||
x, ok := lines[k]
|
||||
if !ok {
|
||||
x = new(record)
|
||||
*x = *rec
|
||||
x.cntr = k
|
||||
}
|
||||
x.count += int(v)
|
||||
lines[k] = x
|
||||
}
|
||||
worku := func(k string, v uint64, rec *record) {
|
||||
work(k, int64(v), rec)
|
||||
}
|
||||
for _, f := range files {
|
||||
if f.counters != nil {
|
||||
var rec record
|
||||
rec.goos = f.counters.Meta["GOOS"]
|
||||
rec.garch = f.counters.Meta["GOARCH"]
|
||||
rec.program = f.counters.Meta["Program"]
|
||||
rec.version = f.counters.Meta["Version"]
|
||||
rec.goversion = f.counters.Meta["GoVersion"]
|
||||
for k, v := range f.counters.Count {
|
||||
worku(k, v, &rec)
|
||||
}
|
||||
} else if f.report != nil {
|
||||
for _, p := range f.report.Programs {
|
||||
var rec record
|
||||
rec.goos = p.GOOS
|
||||
rec.garch = p.GOARCH
|
||||
rec.goversion = p.GoVersion
|
||||
rec.program = p.Program
|
||||
rec.version = p.Version
|
||||
for k, v := range p.Counters {
|
||||
work(k, v, &rec)
|
||||
}
|
||||
for k, v := range p.Stacks {
|
||||
work(k, v, &rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
keys := make([]string, 0, len(lines))
|
||||
for k := range lines {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
printRecord(lines[k])
|
||||
}
|
||||
}
|
||||
|
||||
func printRecord(r *record) {
|
||||
fmt.Printf("%d,%q,%s,%s,%s,%s,%s\n", r.count, r.cntr, r.program,
|
||||
r.version, r.goversion, r.goos, r.garch)
|
||||
}
|
||||
|
||||
func readdir(dir string, files []*file) ([]*file, error) {
|
||||
fi, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, f := range fi {
|
||||
files = append(files, &file{path: filepath.Join(dir, f.Name()), name: f.Name()})
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Go Telemetry View
|
||||
|
||||
Telemetry data it is stored in files on the user machine. Users can run the
|
||||
command `gotelemetry view` to view the data in a browser. The HTML page served
|
||||
by the command will generate graphs based on the local copies of report uploads
|
||||
and active counter files.
|
||||
|
||||
## Development
|
||||
|
||||
The static files are generated with a generator command. You can edit the source
|
||||
files and run go generate to rebuild them.
|
||||
|
||||
go generate ./content
|
||||
|
||||
Running the server with the `--dev` flag will watch and rebuild the static files
|
||||
on save.
|
||||
|
||||
go run ./cmd/gotelemetry view --dev
|
||||
+675
@@ -0,0 +1,675 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The view command is a server intended to be run on a user's machine to
|
||||
// display the local counters and time series charts of counters.
|
||||
package view
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/telemetry/cmd/gotelemetry/internal/browser"
|
||||
"golang.org/x/telemetry/internal/config"
|
||||
"golang.org/x/telemetry/internal/configstore"
|
||||
contentfs "golang.org/x/telemetry/internal/content"
|
||||
tcounter "golang.org/x/telemetry/internal/counter"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/unionfs"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Addr string
|
||||
Dev bool
|
||||
FsConfig string
|
||||
Open bool
|
||||
}
|
||||
|
||||
// Serve starts the telemetry viewer and runs indefinitely.
|
||||
func (s *Server) Serve() {
|
||||
var fsys fs.FS = contentfs.FS
|
||||
if s.Dev {
|
||||
fsys = os.DirFS("internal/content")
|
||||
contentfs.RunESBuild(true)
|
||||
}
|
||||
|
||||
var err error
|
||||
fsys, err = unionfs.Sub(fsys, "gotelemetryview", "shared")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", s.handleIndex(fsys))
|
||||
listener, err := net.Listen("tcp", s.Addr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
addr := fmt.Sprintf("http://%s", listener.Addr())
|
||||
fmt.Printf("server listening at %s\n", addr)
|
||||
if s.Open {
|
||||
browser.Open(addr)
|
||||
}
|
||||
log.Fatal(http.Serve(listener, mux))
|
||||
}
|
||||
|
||||
type page struct {
|
||||
// Config is the config used to render the requested page.
|
||||
Config *config.Config
|
||||
|
||||
// PrettyConfig is the Config struct formatted as indented JSON for display on the page.
|
||||
PrettyConfig string
|
||||
|
||||
// ConfigVersion is used to render a dropdown list of config versions for a user to select.
|
||||
ConfigVersions []string
|
||||
|
||||
// RequestedConfig is the URL query param value for config.
|
||||
RequestedConfig string
|
||||
|
||||
// Files are the local counter files for display on the page.
|
||||
Files []*counterFile
|
||||
|
||||
// Reports are the local reports for display on the page.
|
||||
Reports []*telemetryReport
|
||||
|
||||
// Charts is the counter data from files and reports grouped by program and counter name.
|
||||
Charts *chartdata
|
||||
}
|
||||
|
||||
// TODO: filtering and pagination for date ranges
|
||||
func (s *Server) handleIndex(fsys fs.FS) handlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) error {
|
||||
if r.URL.Path != "/" {
|
||||
http.FileServer(http.FS(fsys)).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
requestedConfig := r.URL.Query().Get("config")
|
||||
if requestedConfig == "" {
|
||||
requestedConfig = "latest"
|
||||
}
|
||||
cfg, err := s.configAt(requestedConfig)
|
||||
if err != nil {
|
||||
log.Printf("Falling back to empty config: %v", err)
|
||||
cfg, _ = s.configAt("empty")
|
||||
}
|
||||
cfgVersionList, err := configVersions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgJSON, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localDir := telemetry.Default.LocalDir()
|
||||
if _, err := os.Stat(localDir); err != nil {
|
||||
return fmt.Errorf(
|
||||
`The telemetry dir %s does not exist.
|
||||
There is nothing to report.`, telemetry.Default.LocalDir())
|
||||
}
|
||||
reports, err := reports(localDir, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files, err := files(localDir, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
charts, err := charts(append(reports, pending(files, cfg)...), cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := page{
|
||||
Config: cfg,
|
||||
PrettyConfig: string(cfgJSON),
|
||||
ConfigVersions: cfgVersionList,
|
||||
Reports: reports,
|
||||
Files: files,
|
||||
Charts: charts,
|
||||
RequestedConfig: requestedConfig,
|
||||
}
|
||||
return renderTemplate(w, fsys, "index.html", data, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
// configAt gets the config at a given version.
|
||||
func (s Server) configAt(version string) (ucfg *config.Config, err error) {
|
||||
if version == "" || version == "empty" {
|
||||
return config.NewConfig(&telemetry.UploadConfig{}), nil
|
||||
}
|
||||
if s.FsConfig != "" {
|
||||
ucfg, err = config.ReadConfig(s.FsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
cfg, _, err := configstore.Download(version, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ucfg = config.NewConfig(cfg)
|
||||
}
|
||||
return ucfg, nil
|
||||
}
|
||||
|
||||
// configVersions is the set of config versions the user may select from the UI.
|
||||
// TODO: get the list of versions available from the proxy.
|
||||
func configVersions() ([]string, error) {
|
||||
v := []string{"latest"}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// reports reads the local report files from a directory.
|
||||
func reports(dir string, cfg *config.Config) ([]*telemetryReport, error) {
|
||||
fsys := os.DirFS(dir)
|
||||
entries, err := fs.ReadDir(fsys, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var reports []*telemetryReport
|
||||
for _, e := range entries {
|
||||
if path.Ext(e.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
data, err := fs.ReadFile(fsys, e.Name())
|
||||
if err != nil {
|
||||
log.Printf("read report file failed: %v", err)
|
||||
continue
|
||||
}
|
||||
var report *telemetry.Report
|
||||
if err := json.Unmarshal(data, &report); err != nil {
|
||||
log.Printf("unmarshal report file %v failed: %v, skipping...", e.Name(), err)
|
||||
continue
|
||||
}
|
||||
wrapped, err := newTelemetryReport(report, cfg)
|
||||
if err != nil {
|
||||
log.Printf("processing report file %v failed: %v, skipping", e.Name(), err)
|
||||
continue
|
||||
}
|
||||
reports = append(reports, wrapped)
|
||||
}
|
||||
// sort the reports descending by week.
|
||||
sort.Slice(reports, func(i, j int) bool {
|
||||
return reports[j].Week < reports[i].Week
|
||||
})
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
// telemetryReport wraps telemetry report to add convenience fields for the UI.
|
||||
type telemetryReport struct {
|
||||
*telemetry.Report
|
||||
ID string
|
||||
WeekEnd time.Time // parsed telemetry.Report.Week
|
||||
Programs []*telemetryProgram
|
||||
}
|
||||
|
||||
type telemetryProgram struct {
|
||||
*telemetry.ProgramReport
|
||||
ID string
|
||||
Summary template.HTML
|
||||
}
|
||||
|
||||
func newTelemetryReport(t *telemetry.Report, cfg *config.Config) (*telemetryReport, error) {
|
||||
weekEnd, err := parseReportDate(t.Week)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected Week %q in the report", t.Week)
|
||||
}
|
||||
var prgms []*telemetryProgram
|
||||
for _, p := range t.Programs {
|
||||
meta := map[string]string{
|
||||
"Program": p.Program,
|
||||
"Version": p.Version,
|
||||
"GOOS": p.GOOS,
|
||||
"GOARCH": p.GOARCH,
|
||||
"GoVersion": p.GoVersion,
|
||||
}
|
||||
counters := make(map[string]uint64)
|
||||
for k, v := range p.Counters {
|
||||
counters[k] = uint64(v)
|
||||
}
|
||||
prgms = append(prgms, &telemetryProgram{
|
||||
ProgramReport: p,
|
||||
ID: strings.Join([]string{"reports", t.Week, p.Program, p.Version, p.GOOS, p.GOARCH, p.GoVersion}, ":"),
|
||||
Summary: summary(cfg, meta, counters),
|
||||
})
|
||||
}
|
||||
return &telemetryReport{
|
||||
Report: t,
|
||||
WeekEnd: weekEnd,
|
||||
ID: "reports:" + t.Week,
|
||||
Programs: prgms,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// files reads the local counter files from a directory.
|
||||
func files(dir string, cfg *config.Config) ([]*counterFile, error) {
|
||||
fsys := os.DirFS(dir)
|
||||
entries, err := fs.ReadDir(fsys, ".")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var files []*counterFile
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || path.Ext(e.Name()) != ".count" {
|
||||
continue
|
||||
}
|
||||
data, err := fs.ReadFile(fsys, e.Name())
|
||||
if err != nil {
|
||||
log.Printf("read counter file failed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
file, err := tcounter.Parse(e.Name(), data)
|
||||
if err != nil {
|
||||
log.Printf("parse counter file failed: %v", err)
|
||||
continue
|
||||
}
|
||||
files = append(files, newCounterFile(e.Name(), file, cfg))
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// counterFile wraps counter file to add convenience fields for the UI.
|
||||
type counterFile struct {
|
||||
*tcounter.File
|
||||
ID string
|
||||
Summary template.HTML
|
||||
ActiveMeta map[string]bool
|
||||
Counts []*count
|
||||
Stacks []*stack
|
||||
}
|
||||
|
||||
type count struct {
|
||||
Name string
|
||||
Value uint64
|
||||
Active bool
|
||||
}
|
||||
|
||||
type stack struct {
|
||||
Name string
|
||||
Trace string
|
||||
Value uint64
|
||||
Active bool
|
||||
}
|
||||
|
||||
func newCounterFile(name string, c *tcounter.File, cfg *config.Config) *counterFile {
|
||||
activeMeta := map[string]bool{
|
||||
"Program": cfg.HasProgram(c.Meta["Program"]),
|
||||
"Version": cfg.HasVersion(c.Meta["Program"], c.Meta["Version"]),
|
||||
"GOOS": cfg.HasGOOS(c.Meta["GOOS"]),
|
||||
"GOARCH": cfg.HasGOARCH(c.Meta["GOARCH"]),
|
||||
"GoVersion": cfg.HasGoVersion(c.Meta["GoVersion"]),
|
||||
}
|
||||
var counts []*count
|
||||
var stacks []*stack
|
||||
for k, v := range c.Count {
|
||||
if summary, details, ok := strings.Cut(k, "\n"); ok {
|
||||
active := cfg.HasStack(c.Meta["Program"], k)
|
||||
stacks = append(stacks, &stack{summary, details, v, active})
|
||||
} else {
|
||||
active := cfg.HasCounter(c.Meta["Program"], k)
|
||||
counts = append(counts, &count{k, v, active})
|
||||
}
|
||||
}
|
||||
sort.Slice(counts, func(i, j int) bool {
|
||||
return counts[i].Name < counts[j].Name
|
||||
})
|
||||
sort.Slice(stacks, func(i, j int) bool {
|
||||
return stacks[i].Name < stacks[j].Name
|
||||
})
|
||||
return &counterFile{
|
||||
File: c,
|
||||
ID: name,
|
||||
ActiveMeta: activeMeta,
|
||||
Counts: counts,
|
||||
Stacks: stacks,
|
||||
Summary: summary(cfg, c.Meta, c.Count),
|
||||
}
|
||||
}
|
||||
|
||||
// summary generates a summary of a set of telemetry data. It describes what data is
|
||||
// located in the set is not allowed given a config and how the data would be handled
|
||||
// in the event of a telemetry upload event.
|
||||
func summary(cfg *config.Config, meta map[string]string, counts map[string]uint64) template.HTML {
|
||||
msg := " is unregistered. No data from this set would be uploaded to the Go team."
|
||||
if prog := meta["Program"]; !(cfg.HasProgram(prog)) {
|
||||
return template.HTML(fmt.Sprintf(
|
||||
"The program <code>%s</code>"+msg,
|
||||
html.EscapeString(prog),
|
||||
))
|
||||
}
|
||||
var result strings.Builder
|
||||
if !(cfg.HasGOOS(meta["GOOS"])) || !(cfg.HasGOARCH(meta["GOARCH"])) {
|
||||
return template.HTML(fmt.Sprintf(
|
||||
"The GOOS/GOARCH combination <code>%s/%s</code> "+msg,
|
||||
html.EscapeString(meta["GOOS"]),
|
||||
html.EscapeString(meta["GOARCH"]),
|
||||
))
|
||||
}
|
||||
goVersion := meta["GoVersion"]
|
||||
if !(cfg.HasGoVersion(goVersion)) {
|
||||
return template.HTML(fmt.Sprintf(
|
||||
"The go version <code>%s</code> "+msg,
|
||||
html.EscapeString(goVersion),
|
||||
))
|
||||
}
|
||||
version := meta["Version"]
|
||||
if !(cfg.HasVersion(meta["Program"], version)) {
|
||||
return template.HTML(fmt.Sprintf(
|
||||
"The version <code>%s</code> "+msg,
|
||||
html.EscapeString(version),
|
||||
))
|
||||
}
|
||||
var counters []string
|
||||
for c := range counts {
|
||||
summary, _, ok := strings.Cut(c, "\n")
|
||||
if ok && !cfg.HasStack(meta["Program"], c) {
|
||||
counters = append(counters, fmt.Sprintf("<code>%s</code>", html.EscapeString(summary)))
|
||||
}
|
||||
if !ok && !(cfg.HasCounter(meta["Program"], c)) {
|
||||
counters = append(counters, fmt.Sprintf("<code>%s</code>", html.EscapeString(c)))
|
||||
}
|
||||
}
|
||||
if len(counters) > 0 {
|
||||
result.WriteString("Unregistered counter(s) ")
|
||||
result.WriteString(strings.Join(counters, ", "))
|
||||
result.WriteString(" would be excluded from a report. ")
|
||||
}
|
||||
return template.HTML(result.String())
|
||||
}
|
||||
|
||||
type chartdata struct {
|
||||
Programs []*program
|
||||
// DateRange is used to align the week intervals for each of the charts.
|
||||
DateRange [2]string
|
||||
// UploadDay is the day of the week the reports are uploaded.
|
||||
// This is used as d3 chart time interval name
|
||||
// to customize the date range bining in the charts.
|
||||
UploadDay string
|
||||
}
|
||||
|
||||
type program struct {
|
||||
ID string
|
||||
Name string
|
||||
Counters []*counter
|
||||
Active bool
|
||||
}
|
||||
|
||||
type counter struct {
|
||||
ID string
|
||||
Name string
|
||||
Data []*datum
|
||||
Active bool
|
||||
}
|
||||
|
||||
type datum struct {
|
||||
Week string // End of the week in UTC. YYYY-MM-DDT00:00:00Z format.
|
||||
Program string
|
||||
Version string
|
||||
GOARCH string
|
||||
GOOS string
|
||||
GoVersion string
|
||||
Key string
|
||||
Value int64
|
||||
}
|
||||
|
||||
// formatDateTime formats the date to the format that
|
||||
// includes time zone. Telemetry uses UTC for date string
|
||||
// parsing, but JavaScript Date parsing uses local time
|
||||
// unless the date string include the time zone info.
|
||||
func formatDateTime(date time.Time) string {
|
||||
return date.Format("2006-01-02T15:04:05Z") // UTC
|
||||
}
|
||||
|
||||
// parseReportDate parses the date string in the format
|
||||
// used byt the telemetry report.
|
||||
func parseReportDate(s string) (time.Time, error) {
|
||||
return time.Parse(telemetry.DateOnly, s)
|
||||
}
|
||||
|
||||
// charts returns chartdata for a set of telemetry reports. It uses the config
|
||||
// to determine if the programs and counters are active.
|
||||
func charts(reports []*telemetryReport, cfg *config.Config) (*chartdata, error) {
|
||||
data := grouped(reports)
|
||||
// domain is a [min, max] array used in d3.js where min is the minimum
|
||||
// observable time and max is the maximum observable time; both values
|
||||
// are inclusive.
|
||||
domain, err := reportsDomain(reports)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &chartdata{
|
||||
DateRange: [2]string{formatDateTime(domain[0]), formatDateTime(domain[1])},
|
||||
UploadDay: strings.ToLower(domain[1].Weekday().String()),
|
||||
}
|
||||
for pg, pgdata := range data {
|
||||
prog := &program{ID: "charts:" + pg.Name, Name: pg.Name, Active: cfg.HasProgram(pg.Name)}
|
||||
result.Programs = append(result.Programs, prog)
|
||||
for c, cdata := range pgdata {
|
||||
count := &counter{
|
||||
ID: "charts:" + pg.Name + ":" + c.Name,
|
||||
Name: c.Name,
|
||||
Data: cdata,
|
||||
Active: cfg.HasCounter(pg.Name, c.Name) || cfg.HasCounterPrefix(pg.Name, c.Name),
|
||||
}
|
||||
prog.Counters = append(prog.Counters, count)
|
||||
sort.Slice(count.Data, func(i, j int) bool {
|
||||
a, err1 := strconv.ParseFloat(count.Data[i].Key, 32)
|
||||
b, err2 := strconv.ParseFloat(count.Data[j].Key, 32)
|
||||
if err1 == nil && err2 == nil {
|
||||
return a < b
|
||||
}
|
||||
return count.Data[i].Key < count.Data[j].Key
|
||||
})
|
||||
}
|
||||
sort.Slice(prog.Counters, func(i, j int) bool {
|
||||
return prog.Counters[i].Name < prog.Counters[j].Name
|
||||
})
|
||||
}
|
||||
sort.Slice(result.Programs, func(i, j int) bool {
|
||||
return result.Programs[i].Name < result.Programs[j].Name
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// reportsDomain computes a common reportsDomain.
|
||||
func reportsDomain(reports []*telemetryReport) ([2]time.Time, error) {
|
||||
var start, end time.Time
|
||||
for _, r := range reports {
|
||||
if start.IsZero() || start.After(r.WeekEnd) {
|
||||
start = r.WeekEnd
|
||||
}
|
||||
if end.IsZero() || r.WeekEnd.After(end) {
|
||||
end = r.WeekEnd
|
||||
}
|
||||
}
|
||||
if start.IsZero() || end.IsZero() {
|
||||
return [2]time.Time{}, fmt.Errorf("no report with valid Week data")
|
||||
}
|
||||
start = start.AddDate(0, 0, -7) // 7 days before the first report.
|
||||
return [2]time.Time{start, end}, nil
|
||||
}
|
||||
|
||||
type programKey struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
type counterKey struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// grouped returns normalized counter data grouped by program and counter.
|
||||
func grouped(reports []*telemetryReport) map[programKey]map[counterKey][]*datum {
|
||||
result := make(map[programKey]map[counterKey][]*datum)
|
||||
for _, r := range reports {
|
||||
// Adjust the Week string to include the time zone info.
|
||||
// JS's Date.parse uses local time, otherwise.
|
||||
//
|
||||
// r.Week is the end of the week interval in UTC.
|
||||
// If r.Week is 2024-01-08, the report is the data
|
||||
// for the d3 domain[2024-01-01T00:00:00Z, 2024-01-08T00:00:00Z).
|
||||
// Note: the end is exclusive.
|
||||
// To make the report data align with the d3 domain,
|
||||
// adjust the time to the start of the week interval.
|
||||
weekStart := formatDateTime(r.WeekEnd.AddDate(0, 0, -7))
|
||||
for _, e := range r.Programs {
|
||||
pgkey := programKey{e.Program}
|
||||
if _, ok := result[pgkey]; !ok {
|
||||
result[pgkey] = make(map[counterKey][]*datum)
|
||||
}
|
||||
for counter, value := range e.Counters {
|
||||
name, bucket, found := strings.Cut(counter, ":")
|
||||
key := name
|
||||
if found {
|
||||
key = bucket
|
||||
}
|
||||
element := &datum{
|
||||
Week: weekStart,
|
||||
Program: e.Program,
|
||||
Version: e.Version,
|
||||
GOARCH: e.GOARCH,
|
||||
GOOS: e.GOOS,
|
||||
GoVersion: e.GoVersion,
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
ckey := counterKey{name}
|
||||
result[pgkey][ckey] = append(result[pgkey][ckey], element)
|
||||
}
|
||||
for counter, value := range e.Stacks {
|
||||
summary, _, _ := strings.Cut(counter, "\n")
|
||||
element := &datum{
|
||||
Week: weekStart,
|
||||
Program: e.Program,
|
||||
Version: e.Version,
|
||||
GOARCH: e.GOARCH,
|
||||
GOOS: e.GOOS,
|
||||
GoVersion: e.GoVersion,
|
||||
Key: summary,
|
||||
Value: value,
|
||||
}
|
||||
ckey := counterKey{summary}
|
||||
result[pgkey][ckey] = append(result[pgkey][ckey], element)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// pending transforms the active counter files into a report. Used to add
|
||||
// the data they contain to the charts in the UI.
|
||||
func pending(files []*counterFile, cfg *config.Config) []*telemetryReport {
|
||||
reports := make(map[string]*telemetry.Report)
|
||||
for _, f := range files {
|
||||
tb, err := time.Parse(time.RFC3339, f.Meta["TimeEnd"])
|
||||
if err != nil {
|
||||
log.Printf("skipping malformed %v: unexpected TimeEnd value %q", f.ID, f.Meta["TimeEnd"])
|
||||
continue
|
||||
}
|
||||
week := tb.Format(telemetry.DateOnly)
|
||||
if _, ok := reports[week]; !ok {
|
||||
reports[week] = &telemetry.Report{Week: week}
|
||||
}
|
||||
program := &telemetry.ProgramReport{
|
||||
Program: f.Meta["Program"],
|
||||
GOOS: f.Meta["GOOS"],
|
||||
GOARCH: f.Meta["GOARCH"],
|
||||
GoVersion: f.Meta["GoVersion"],
|
||||
Version: f.Meta["Version"],
|
||||
}
|
||||
program.Counters = make(map[string]int64)
|
||||
program.Stacks = make(map[string]int64)
|
||||
for k, v := range f.Count {
|
||||
if tcounter.IsStackCounter(k) {
|
||||
program.Stacks[k] = int64(v)
|
||||
} else {
|
||||
program.Counters[k] = int64(v)
|
||||
}
|
||||
}
|
||||
reports[week].Programs = append(reports[week].Programs, program)
|
||||
}
|
||||
var result []*telemetryReport
|
||||
for _, r := range reports {
|
||||
wrapped, err := newTelemetryReport(r, cfg)
|
||||
if err != nil {
|
||||
log.Printf("skipping the invalid report from week %v: %v", r.Week, err)
|
||||
continue
|
||||
}
|
||||
result = append(result, wrapped)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
type handlerFunc func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
func (f handlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := f(w, r); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// renderTemplate executes a template response.
|
||||
func renderTemplate(w http.ResponseWriter, fsys fs.FS, tmplPath string, data any, code int) error {
|
||||
patterns, err := tmplPatterns(fsys, tmplPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
patterns = append(patterns, tmplPath)
|
||||
funcs := template.FuncMap{
|
||||
"chartName": func(name string) string {
|
||||
name, _, _ = strings.Cut(name, ":")
|
||||
return name
|
||||
},
|
||||
"programName": func(name string) string {
|
||||
name = strings.TrimPrefix(name, "golang.org/")
|
||||
name = strings.TrimPrefix(name, "github.com/")
|
||||
return name
|
||||
},
|
||||
}
|
||||
tmpl, err := template.New("").Funcs(funcs).ParseFS(fsys, patterns...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name := path.Base(tmplPath)
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
||||
return err
|
||||
}
|
||||
if code != 0 {
|
||||
w.WriteHeader(code)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(buf.Len()))
|
||||
if _, err := w.Write(buf.Bytes()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tmplPatterns generates a slice of file patterns to use in template.ParseFS.
|
||||
func tmplPatterns(fsys fs.FS, tmplPath string) ([]string, error) {
|
||||
var patterns []string
|
||||
globs := []string{"*.tmpl", path.Join(path.Dir(tmplPath), "*.tmpl")}
|
||||
for _, g := range globs {
|
||||
matches, err := fs.Glob(fsys, g)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patterns = append(patterns, matches...)
|
||||
}
|
||||
return patterns, nil
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The view command is a server intended to be run on a users machine to
|
||||
// display the local counters and time series charts of counters.
|
||||
package view
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/telemetry/internal/config"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
func Test_summary(t *testing.T) {
|
||||
type args struct {
|
||||
cfg *config.Config
|
||||
meta map[string]string
|
||||
counts map[string]uint64
|
||||
}
|
||||
cfg := config.NewConfig(&telemetry.UploadConfig{
|
||||
GOOS: []string{"linux"},
|
||||
GOARCH: []string{"amd64"},
|
||||
GoVersion: []string{"go1.20.1"},
|
||||
Programs: []*telemetry.ProgramConfig{
|
||||
{
|
||||
Name: "gopls",
|
||||
Versions: []string{"v1.2.3"},
|
||||
Counters: []telemetry.CounterConfig{
|
||||
{Name: "editor"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want template.HTML
|
||||
}{
|
||||
{
|
||||
"empty summary",
|
||||
args{
|
||||
cfg: cfg,
|
||||
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.20.1"},
|
||||
counts: map[string]uint64{"editor": 10},
|
||||
},
|
||||
template.HTML(""),
|
||||
},
|
||||
{
|
||||
"empty config/unknown program",
|
||||
args{
|
||||
cfg: config.NewConfig(&telemetry.UploadConfig{}),
|
||||
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.20.1"},
|
||||
counts: map[string]uint64{"editor": 10},
|
||||
},
|
||||
template.HTML("The program <code>gopls</code> is unregistered. No data from this set would be uploaded to the Go team."),
|
||||
},
|
||||
{
|
||||
"unknown counter",
|
||||
args{
|
||||
cfg: cfg,
|
||||
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.20.1"},
|
||||
counts: map[string]uint64{"editor": 10, "foobar": 10},
|
||||
},
|
||||
template.HTML("Unregistered counter(s) <code>foobar</code> would be excluded from a report. "),
|
||||
},
|
||||
{
|
||||
"unknown goos",
|
||||
args{
|
||||
cfg: cfg,
|
||||
meta: map[string]string{"Program": "gopls", "Version": "v1.2.3", "GOOS": "windows", "GOARCH": "arm64", "GoVersion": "go1.20.1"},
|
||||
counts: map[string]uint64{"editor": 10, "foobar": 10},
|
||||
},
|
||||
template.HTML("The GOOS/GOARCH combination <code>windows/arm64</code> is unregistered. No data from this set would be uploaded to the Go team."),
|
||||
},
|
||||
{
|
||||
"multiple unknown fields",
|
||||
args{
|
||||
cfg: cfg,
|
||||
meta: map[string]string{"Program": "gopls", "Version": "v1.2.5", "GOOS": "linux", "GOARCH": "amd64", "GoVersion": "go1.25.1"},
|
||||
counts: map[string]uint64{"editor": 10, "foobar": 10},
|
||||
},
|
||||
template.HTML("The go version <code>go1.25.1</code> is unregistered. No data from this set would be uploaded to the Go team."),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := summary(tt.args.cfg, tt.args.meta, tt.args.counts)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("summary() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_reportsDomain(t *testing.T) {
|
||||
mustParseDate := func(date string) time.Time {
|
||||
ts, err := time.Parse(telemetry.DateOnly, date)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse date %q: %v", date, err)
|
||||
}
|
||||
return ts
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reportDates []string
|
||||
want [2]time.Time
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "one",
|
||||
reportDates: []string{"2024-01-08"},
|
||||
want: [2]time.Time{mustParseDate("2024-01-01"), mustParseDate("2024-01-08")},
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
reportDates: []string{"2024-04-08", "2024-06-01"},
|
||||
want: [2]time.Time{mustParseDate("2024-04-01"), mustParseDate("2024-06-01")},
|
||||
},
|
||||
{
|
||||
name: "three",
|
||||
reportDates: []string{"2024-04-08", "2024-01-08", "2024-06-01"},
|
||||
want: [2]time.Time{mustParseDate("2024-01-01"), mustParseDate("2024-06-01")},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
reports := make([]*telemetryReport, len(tt.reportDates))
|
||||
for i, date := range tt.reportDates {
|
||||
weekEnd, err := parseReportDate(date)
|
||||
if err != nil {
|
||||
t.Fatalf("parseReport(%v) failed: %v", date, err)
|
||||
}
|
||||
reports[i] = &telemetryReport{
|
||||
WeekEnd: weekEnd,
|
||||
ID: fmt.Sprintf("report-%d", i),
|
||||
}
|
||||
}
|
||||
got, err := reportsDomain(reports)
|
||||
if tt.wantErr && err == nil ||
|
||||
err == nil && !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("reportsDomain() = (%v, %v), want (%v, err=%v)", got, err, tt.want, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+395
@@ -0,0 +1,395 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:generate go test -run=TestDocHelp -update
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/telemetry/cmd/gotelemetry/internal/csv"
|
||||
"golang.org/x/telemetry/cmd/gotelemetry/internal/view"
|
||||
"golang.org/x/telemetry/internal/counter"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/upload"
|
||||
)
|
||||
|
||||
type command struct {
|
||||
usage string
|
||||
short string
|
||||
long string
|
||||
flags *flag.FlagSet
|
||||
hasArgs bool
|
||||
run func([]string)
|
||||
}
|
||||
|
||||
func (c command) name() string {
|
||||
name, _, _ := strings.Cut(c.usage, " ")
|
||||
return name
|
||||
}
|
||||
|
||||
var (
|
||||
viewFlags = flag.NewFlagSet("view", flag.ExitOnError)
|
||||
viewServer view.Server
|
||||
normalCommands = []*command{
|
||||
{
|
||||
usage: "on",
|
||||
short: "enable telemetry collection and uploading",
|
||||
long: `Gotelemetry on enables telemetry collection and uploading.
|
||||
|
||||
When telemetry is enabled, telemetry data is written to the local file system and periodically sent to https://telemetry.go.dev/. Uploaded data is used to help improve the Go toolchain and related tools, and it will be published as part of a public dataset.
|
||||
|
||||
For more details, see https://telemetry.go.dev/privacy.
|
||||
This data is collected in accordance with the Google Privacy Policy (https://policies.google.com/privacy).
|
||||
|
||||
To disable telemetry uploading, but keep local data collection, run “gotelemetry local”.
|
||||
To disable both collection and uploading, run “gotelemetry off“.
|
||||
`,
|
||||
run: runOn,
|
||||
},
|
||||
{
|
||||
usage: "local",
|
||||
short: "enable telemetry collection but disable uploading",
|
||||
long: `Gotelemetry local enables telemetry collection but not uploading.
|
||||
|
||||
When telemetry is in local mode, counter data is written to the local file system, but will not be uploaded to remote servers.
|
||||
|
||||
To enable telemetry uploading, run “gotelemetry on”.
|
||||
To disable both collection and uploading, run “gotelemetry off”`,
|
||||
run: runLocal,
|
||||
},
|
||||
{
|
||||
usage: "off",
|
||||
short: "disable telemetry collection and uploading",
|
||||
long: `Gotelemetry off disables telemetry collection and uploading.
|
||||
|
||||
When telemetry is disabled, local counter data is neither collected nor uploaded.
|
||||
|
||||
To enable local collection (but not uploading) of telemetry data, run “gotelemetry local“.
|
||||
To enable both collection and uploading, run “gotelemetry on”.`,
|
||||
run: runOff,
|
||||
},
|
||||
{
|
||||
usage: "view [flags]",
|
||||
short: "run a web viewer for local telemetry data",
|
||||
long: `Gotelemetry view runs a web viewer for local telemetry data.
|
||||
|
||||
This viewer displays charts for locally collected data, as well as information about the current upload configuration.`,
|
||||
flags: viewFlags,
|
||||
run: runView,
|
||||
},
|
||||
{
|
||||
usage: "env",
|
||||
short: "print the current telemetry environment",
|
||||
run: runEnv,
|
||||
},
|
||||
{
|
||||
usage: "clean",
|
||||
short: "remove all local telemetry data",
|
||||
long: `Gotelemetry clean removes locally collected counters and reports.
|
||||
|
||||
Removing counter files that are currently in use may fail on some operating
|
||||
systems.
|
||||
|
||||
Gotelemetry clean does not affect the current telemetry mode.`,
|
||||
run: runClean,
|
||||
},
|
||||
}
|
||||
experimentalCommands = []*command{
|
||||
{
|
||||
usage: "csv",
|
||||
short: "print all known counters",
|
||||
run: runCSV,
|
||||
},
|
||||
{
|
||||
usage: "dump [files]",
|
||||
short: "view counter file data",
|
||||
run: runDump,
|
||||
hasArgs: true,
|
||||
},
|
||||
{
|
||||
usage: "upload",
|
||||
short: "run upload with logging enabled",
|
||||
run: runUpload,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
viewFlags.StringVar(&viewServer.Addr, "addr", "localhost:4040", "server listens on the given TCP network address")
|
||||
viewFlags.BoolVar(&viewServer.Dev, "dev", false, "rebuild static assets on save")
|
||||
viewFlags.StringVar(&viewServer.FsConfig, "config", "", "load a config from the filesystem")
|
||||
viewFlags.BoolVar(&viewServer.Open, "open", true, "open the browser to the server address")
|
||||
|
||||
for _, cmd := range append(normalCommands, experimentalCommands...) {
|
||||
name := cmd.name()
|
||||
if cmd.flags == nil {
|
||||
cmd.flags = flag.NewFlagSet(name, flag.ExitOnError)
|
||||
}
|
||||
cmd.flags.Usage = func() {
|
||||
help(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func output(msgs ...any) {
|
||||
fmt.Fprintln(flag.CommandLine.Output(), msgs...)
|
||||
}
|
||||
|
||||
func usage() {
|
||||
printCommand := func(cmd *command) {
|
||||
output(fmt.Sprintf("\t%s\t%s", cmd.name(), cmd.short))
|
||||
}
|
||||
output("Gotelemetry is a tool for managing Go telemetry data and settings.")
|
||||
output()
|
||||
output("Usage:")
|
||||
output()
|
||||
output("\tgotelemetry <command> [arguments]")
|
||||
output()
|
||||
output("The commands are:")
|
||||
output()
|
||||
for _, cmd := range normalCommands {
|
||||
printCommand(cmd)
|
||||
}
|
||||
output()
|
||||
output("Use \"gotelemetry help <command>\" for details about any command.")
|
||||
output()
|
||||
output("The following additional commands are available for diagnostic")
|
||||
output("purposes, and may change or be removed in the future:")
|
||||
output()
|
||||
for _, cmd := range experimentalCommands {
|
||||
printCommand(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func failf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func warnf(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, "Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
func findCommand(name string) *command {
|
||||
for _, cmd := range append(normalCommands, experimentalCommands...) {
|
||||
if cmd.name() == name {
|
||||
return cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func help(name string) {
|
||||
cmd := findCommand(name)
|
||||
if cmd == nil {
|
||||
failf("unknown command %q", name)
|
||||
}
|
||||
output(fmt.Sprintf("Usage: gotelemetry %s", cmd.usage))
|
||||
output()
|
||||
if cmd.long != "" {
|
||||
output(cmd.long)
|
||||
} else {
|
||||
output(fmt.Sprintf("Gotelemetry %s is used to %s.", cmd.name(), cmd.short))
|
||||
}
|
||||
anyflags := false
|
||||
cmd.flags.VisitAll(func(*flag.Flag) {
|
||||
anyflags = true
|
||||
})
|
||||
if anyflags {
|
||||
output()
|
||||
output("Flags:")
|
||||
output()
|
||||
cmd.flags.PrintDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
func runOn(_ []string) {
|
||||
if old, _ := telemetry.Default.Mode(); old == "on" {
|
||||
return
|
||||
}
|
||||
if err := telemetry.Default.SetMode("on"); err != nil {
|
||||
failf("Failed to enable telemetry: %v", err)
|
||||
}
|
||||
// We could perhaps only show the telemetry on message when the mode goes
|
||||
// from off->on (i.e. check the previous state before calling setMode),
|
||||
// but that seems like an unnecessary optimization.
|
||||
fmt.Fprintln(os.Stderr, telemetryOnMessage())
|
||||
}
|
||||
|
||||
func telemetryOnMessage() string {
|
||||
return `Telemetry uploading is now enabled.
|
||||
Data will be sent periodically to https://telemetry.go.dev/.
|
||||
Uploaded data is used to help improve the Go toolchain and related tools,
|
||||
and it will be published as part of a public dataset.
|
||||
|
||||
For more details, see https://telemetry.go.dev/privacy.
|
||||
This data is collected in accordance with the Google Privacy Policy
|
||||
(https://policies.google.com/privacy).
|
||||
|
||||
To disable telemetry uploading, but keep local data collection,
|
||||
run “gotelemetry local”.
|
||||
To disable both collection and uploading, run “gotelemetry off“.`
|
||||
}
|
||||
|
||||
func runLocal(_ []string) {
|
||||
if old, _ := telemetry.Default.Mode(); old == "local" {
|
||||
return
|
||||
}
|
||||
if err := telemetry.Default.SetMode("local"); err != nil {
|
||||
failf("Failed to set the telemetry mode to local: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runOff(_ []string) {
|
||||
if old, _ := telemetry.Default.Mode(); old == "off" {
|
||||
return
|
||||
}
|
||||
if err := telemetry.Default.SetMode("off"); err != nil {
|
||||
failf("Failed to disable telemetry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runView(_ []string) {
|
||||
viewServer.Serve()
|
||||
}
|
||||
|
||||
func runEnv(_ []string) {
|
||||
m, t := telemetry.Default.Mode()
|
||||
fmt.Printf("mode: %s %s\n", m, t)
|
||||
fmt.Println()
|
||||
fmt.Println("modefile:", telemetry.Default.ModeFile())
|
||||
fmt.Println("localdir:", telemetry.Default.LocalDir())
|
||||
fmt.Println("uploaddir:", telemetry.Default.UploadDir())
|
||||
}
|
||||
|
||||
func runClean(_ []string) {
|
||||
// For now, be careful to only remove counter files and reports.
|
||||
// It would probably be OK to just remove everything, but it may
|
||||
// be useful to preserve the weekends file.
|
||||
for dir, suffixes := range map[string][]string{
|
||||
telemetry.Default.LocalDir(): {"." + counter.FileVersion + ".count", ".json"},
|
||||
telemetry.Default.UploadDir(): {".json"},
|
||||
} {
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
warnf("failed to read telemetry dir: %v", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
// TODO: use slices.ContainsFunc once it is available in all supported Go
|
||||
// versions.
|
||||
remove := false
|
||||
for _, suffix := range suffixes {
|
||||
if strings.HasSuffix(entry.Name(), suffix) {
|
||||
remove = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if remove {
|
||||
path := filepath.Join(dir, entry.Name())
|
||||
if err := os.Remove(path); err != nil {
|
||||
warnf("failed to remove %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runCSV(_ []string) {
|
||||
csv.Csv()
|
||||
}
|
||||
|
||||
func runDump(args []string) {
|
||||
if len(args) == 0 {
|
||||
localdir := telemetry.Default.LocalDir()
|
||||
fi, err := os.ReadDir(localdir)
|
||||
if err != nil && len(args) == 0 {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, f := range fi {
|
||||
args = append(args, filepath.Join(localdir, f.Name()))
|
||||
}
|
||||
}
|
||||
for _, file := range args {
|
||||
if !strings.HasSuffix(file, ".count") {
|
||||
log.Printf("%s: not a counter file, skipping", file)
|
||||
continue
|
||||
}
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
log.Printf("%v, skipping", err)
|
||||
continue
|
||||
}
|
||||
f, err := counter.Parse(file, data)
|
||||
if err != nil {
|
||||
log.Printf("%v, skipping", err)
|
||||
continue
|
||||
}
|
||||
js, err := json.MarshalIndent(f, "", "\t")
|
||||
if err != nil {
|
||||
log.Printf("%s: failed to print - %v", file, err)
|
||||
}
|
||||
fmt.Printf("-- %v --\n%s\n", file, js)
|
||||
}
|
||||
}
|
||||
|
||||
func runUpload(_ []string) {
|
||||
if err := upload.Run(upload.RunConfig{
|
||||
LogWriter: os.Stderr,
|
||||
}); err != nil {
|
||||
fmt.Printf("Upload failed: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Upload completed.")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
if flag.NArg() == 0 {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if args[0] == "help" {
|
||||
flag.CommandLine.SetOutput(os.Stdout)
|
||||
switch len(args) {
|
||||
case 1:
|
||||
flag.Usage()
|
||||
case 2:
|
||||
help(args[1])
|
||||
default:
|
||||
flag.Usage()
|
||||
failf("too many arguments to \"help\"")
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
cmd := findCommand(args[0])
|
||||
if cmd == nil {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
cmd.flags.Parse(args[1:]) // will exit on error
|
||||
args = cmd.flags.Args()
|
||||
if !cmd.hasArgs && len(args) > 0 {
|
||||
help(cmd.name())
|
||||
failf("command %s does not accept any arguments", cmd.name())
|
||||
}
|
||||
cmd.run(args)
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter
|
||||
|
||||
// The implementation of this package and tests are located in
|
||||
// internal/counter, which can be shared with the upload package.
|
||||
// TODO(hyangah): use of type aliases prevents nice documentation
|
||||
// rendering in go doc or pkgsite. Fix this either by avoiding
|
||||
// type aliasing or restructuring the internal/counter package.
|
||||
import (
|
||||
"flag"
|
||||
"path"
|
||||
"runtime/debug"
|
||||
|
||||
"golang.org/x/telemetry/internal/counter"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
// Inc increments the counter with the given name.
|
||||
func Inc(name string) {
|
||||
New(name).Inc()
|
||||
}
|
||||
|
||||
// Add adds n to the counter with the given name.
|
||||
func Add(name string, n int64) {
|
||||
New(name).Add(n)
|
||||
}
|
||||
|
||||
// New returns a counter with the given name.
|
||||
// New can be called in global initializers and will be compiled down to
|
||||
// linker-initialized data. That is, calling New to initialize a global
|
||||
// has no cost at program startup.
|
||||
//
|
||||
// See "Counter Naming" in the package doc for a description of counter naming
|
||||
// conventions.
|
||||
func New(name string) *Counter {
|
||||
// Note: not calling DefaultFile.New in order to keep this
|
||||
// function something the compiler can inline and convert
|
||||
// into static data initializations, with no init-time footprint.
|
||||
// TODO(hyangah): is it trivial enough for the compiler to inline?
|
||||
return counter.New(name)
|
||||
}
|
||||
|
||||
// A Counter is a single named event counter.
|
||||
// A Counter is safe for use by multiple goroutines simultaneously.
|
||||
//
|
||||
// Counters should typically be created using New
|
||||
// and stored as global variables, like:
|
||||
//
|
||||
// package mypackage
|
||||
// var errorCount = counter.New("mypackage/errors")
|
||||
//
|
||||
// (The initialization of errorCount in this example is handled
|
||||
// entirely by the compiler and linker; this line executes no code
|
||||
// at program startup.)
|
||||
//
|
||||
// Then code can call Add to increment the counter
|
||||
// each time the corresponding event is observed.
|
||||
//
|
||||
// Although it is possible to use New to create
|
||||
// a Counter each time a particular event needs to be recorded,
|
||||
// that usage fails to amortize the construction cost over
|
||||
// multiple calls to Add, so it is more expensive and not recommended.
|
||||
type Counter = counter.Counter
|
||||
|
||||
// A StackCounter is the in-memory knowledge about a stack counter.
|
||||
// StackCounters are more expensive to use than regular Counters,
|
||||
// requiring, at a minimum, a call to runtime.Callers.
|
||||
type StackCounter = counter.StackCounter
|
||||
|
||||
// NewStack returns a new stack counter with the given name and depth.
|
||||
//
|
||||
// See "Counter Naming" in the package doc for a description of counter naming
|
||||
// conventions.
|
||||
func NewStack(name string, depth int) *StackCounter {
|
||||
return counter.NewStack(name, depth)
|
||||
}
|
||||
|
||||
// Open prepares telemetry counters for recording to the file system.
|
||||
//
|
||||
// If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
|
||||
// counter file on disk and starts to mmap telemetry counters to the file.
|
||||
// Open also persists any counters already created in the current process.
|
||||
//
|
||||
// Open should only be called from short-lived processes such as command line
|
||||
// tools. If your process is long-running, use [OpenAndRotate].
|
||||
func Open() {
|
||||
counter.Open(false)
|
||||
}
|
||||
|
||||
// OpenAndRotate is like [Open], but also schedules a rotation of the counter
|
||||
// file when it expires.
|
||||
//
|
||||
// See golang/go#68497 for background on why [OpenAndRotate] is a separate API.
|
||||
//
|
||||
// TODO(rfindley): refactor Open and OpenAndRotate for Go 1.24.
|
||||
func OpenAndRotate() {
|
||||
counter.Open(true)
|
||||
}
|
||||
|
||||
// OpenDir prepares telemetry counters for recording to the file system, using
|
||||
// the specified telemetry directory, if it is not the empty string.
|
||||
//
|
||||
// If the telemetry mode is "off", Open is a no-op. Otherwise, it opens the
|
||||
// counter file on disk and starts to mmap telemetry counters to the file.
|
||||
// Open also persists any counters already created in the current process.
|
||||
func OpenDir(telemetryDir string) {
|
||||
if telemetryDir != "" {
|
||||
telemetry.Default = telemetry.NewDir(telemetryDir)
|
||||
}
|
||||
counter.Open(false)
|
||||
}
|
||||
|
||||
// CountFlags creates a counter for every flag that is set
|
||||
// and increments the counter. The name of the counter is
|
||||
// the concatenation of prefix and the flag name.
|
||||
//
|
||||
// For instance, CountFlags("gopls/flag:", *flag.CommandLine)
|
||||
func CountFlags(prefix string, fs flag.FlagSet) {
|
||||
fs.Visit(func(f *flag.Flag) {
|
||||
New(prefix + f.Name).Inc()
|
||||
})
|
||||
}
|
||||
|
||||
// CountCommandLineFlags creates a counter for every flag
|
||||
// that is set in the default flag.CommandLine FlagSet using
|
||||
// the counter name binaryName+"/flag:"+flagName where
|
||||
// binaryName is the base name of the Path embedded in the
|
||||
// binary's build info. If the binary does not have embedded build
|
||||
// info, the "flag:"+flagName counter will be incremented.
|
||||
//
|
||||
// CountCommandLineFlags must be called after flags are parsed
|
||||
// with flag.Parse.
|
||||
//
|
||||
// For instance, if the -S flag is passed to cmd/compile and
|
||||
// CountCommandLineFlags is called after flags are parsed,
|
||||
// the "compile/flag:S" counter will be incremented.
|
||||
func CountCommandLineFlags() {
|
||||
prefix := "flag:"
|
||||
if buildInfo, ok := debug.ReadBuildInfo(); ok && buildInfo.Path != "" {
|
||||
prefix = path.Base(buildInfo.Path) + "/" + prefix
|
||||
}
|
||||
CountFlags(prefix, *flag.CommandLine)
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package counter_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/counter"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/testenv"
|
||||
)
|
||||
|
||||
const telemetryDirEnvVar = "_COUNTER_TEST_TELEMETRY_DIR"
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if dir := os.Getenv(telemetryDirEnvVar); dir != "" {
|
||||
// Run for TestOpenAPIMisuse.
|
||||
telemetry.Default = telemetry.NewDir(dir)
|
||||
counter.Open()
|
||||
counter.OpenAndRotate() // should panic
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestOpenAPIMisuse(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
|
||||
// Test that Open and OpenAndRotate cannot be used simultaneously.
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(exe)
|
||||
cmd.Env = append(os.Environ(), telemetryDirEnvVar+"="+t.TempDir())
|
||||
|
||||
if err := cmd.Run(); err == nil {
|
||||
t.Error("Failed to detect API misuse: no error from calling both Open and OpenAndRotate")
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// countertest provides testing utilities for counters.
|
||||
// This package cannot be used except for testing.
|
||||
package countertest
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"golang.org/x/telemetry/counter"
|
||||
ic "golang.org/x/telemetry/internal/counter"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
var (
|
||||
openedMu sync.Mutex
|
||||
opened bool
|
||||
)
|
||||
|
||||
// SupportedPlatform reports if this platform supports Open()
|
||||
const SupportedPlatform = !telemetry.DisabledOnPlatform
|
||||
|
||||
func isOpen() bool {
|
||||
openedMu.Lock()
|
||||
defer openedMu.Unlock()
|
||||
return opened
|
||||
}
|
||||
|
||||
// Open enables telemetry data writing to disk.
|
||||
// This is supposed to be called once during the program execution
|
||||
// (i.e. typically in TestMain), and must not be used with
|
||||
// golang.org/x/telemetry/counter.Open.
|
||||
func Open(telemetryDir string) {
|
||||
openedMu.Lock()
|
||||
defer openedMu.Unlock()
|
||||
if opened {
|
||||
panic("Open was called more than once")
|
||||
}
|
||||
telemetry.Default = telemetry.NewDir(telemetryDir)
|
||||
|
||||
// TODO(rfindley): reinstate test coverage with counter rotation enabled.
|
||||
// Before the [counter.Open] and [counter.OpenAndRotate] APIs were split,
|
||||
// this called counter.Open (which rotated!).
|
||||
counter.Open()
|
||||
opened = true
|
||||
}
|
||||
|
||||
// ReadCounter reads the given counter.
|
||||
func ReadCounter(c *counter.Counter) (count uint64, _ error) {
|
||||
return ic.Read(c)
|
||||
}
|
||||
|
||||
// ReadStackCounter reads the given StackCounter.
|
||||
func ReadStackCounter(c *counter.StackCounter) (stackCounts map[string]uint64, _ error) {
|
||||
return ic.ReadStack(c)
|
||||
}
|
||||
|
||||
// ReadFile reads the counters and stack counters from the given file.
|
||||
func ReadFile(name string) (counters, stackCounters map[string]uint64, _ error) {
|
||||
return ic.ReadFile(name)
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.21
|
||||
|
||||
package countertest
|
||||
|
||||
import "testing"
|
||||
|
||||
func init() {
|
||||
// Extra safety check for go1.21+.
|
||||
if !testing.Testing() {
|
||||
panic("use of this package is disallowed in non-testing code")
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.21
|
||||
|
||||
package countertest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/counter"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/testenv"
|
||||
)
|
||||
|
||||
func TestReadCounter(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
c := counter.New("foobar")
|
||||
|
||||
got, err := ReadCounter(c)
|
||||
if err != nil {
|
||||
t.Errorf("ReadCounter = (%d, %v), want (0,nil)", got, err)
|
||||
}
|
||||
if got != 0 {
|
||||
t.Fatalf("ReadCounter = %d, want 0", got)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
for i := 0; i < 100; i++ {
|
||||
go func() {
|
||||
c.Inc()
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
if got, err := ReadCounter(c); err != nil || got != 100 {
|
||||
t.Errorf("ReadCounter = (%v, %v), want (%v, nil)", got, err, 100)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadStackCounter(t *testing.T) {
|
||||
testenv.SkipIfUnsupportedPlatform(t)
|
||||
c := counter.NewStack("foobar", 8)
|
||||
|
||||
if got, err := ReadStackCounter(c); err != nil || len(got) != 0 {
|
||||
t.Errorf("ReadStackCounter = (%q, %v), want (%v, nil)", got, err, 0)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(100)
|
||||
for i := 0; i < 100; i++ {
|
||||
go func() {
|
||||
c.Inc() // one stack!
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
got, err := ReadStackCounter(c)
|
||||
if err != nil || len(got) != 1 {
|
||||
t.Fatalf("ReadStackCounter = (%v, %v), want to read one entry", stringify(got), err)
|
||||
}
|
||||
for k, v := range got {
|
||||
if !strings.Contains(k, t.Name()) || v != 100 {
|
||||
t.Fatalf("ReadStackCounter = %v, want a stack counter with value 100", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSupport(t *testing.T) {
|
||||
if SupportedPlatform == telemetry.DisabledOnPlatform {
|
||||
t.Errorf("supported mismatch: us %v, telemetry.internal.Disabled %v",
|
||||
SupportedPlatform, telemetry.DisabledOnPlatform)
|
||||
}
|
||||
}
|
||||
|
||||
func stringify(m map[string]uint64) string {
|
||||
kv := make([]string, 0, len(m))
|
||||
for k, v := range m {
|
||||
kv = append(kv, fmt.Sprintf("%q:%v", k, v))
|
||||
}
|
||||
slices.Sort(kv)
|
||||
return "{" + strings.Join(kv, " ") + "}"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package counter implements a simple counter system for collecting
|
||||
// totally public telemetry data.
|
||||
//
|
||||
// There are two kinds of counters, basic counters and stack counters.
|
||||
// Basic counters are created by [New].
|
||||
// Stack counters are created by [NewStack].
|
||||
// Both are incremented by calling Inc().
|
||||
//
|
||||
// Basic counters are very cheap. Stack counters are more expensive, as they
|
||||
// require parsing the stack. (Stack counters are implemented as basic counters
|
||||
// whose names are the concatenation of the name and the stack trace. There is
|
||||
// an upper limit on the size of this name, about 4K bytes. If the name is too
|
||||
// long the stack will be truncated and "truncated" appended.)
|
||||
//
|
||||
// When counter files expire they are turned into reports by the upload
|
||||
// package. The first time any counter file is created for a user, a random day
|
||||
// of the week is selected on which counter files will expire. For the first
|
||||
// week, that day is more than 7 days (but not more than two weeks) in the
|
||||
// future. After that the counter files expire weekly on the same day of the
|
||||
// week.
|
||||
//
|
||||
// # Counter Naming
|
||||
//
|
||||
// Counter names passed to [New] and [NewStack] should follow these
|
||||
// conventions:
|
||||
//
|
||||
// - Names cannot contain whitespace or newlines.
|
||||
//
|
||||
// - Names must be valid unicode, with no unprintable characters.
|
||||
//
|
||||
// - Names may contain at most one ':'. In the counter "foo:bar", we refer to
|
||||
// "foo" as the "chart name" and "bar" as the "bucket name".
|
||||
//
|
||||
// - The '/' character should partition counter names into a hierarchy. The
|
||||
// root of this hierarchy should identify the logical entity that "owns"
|
||||
// the counter. This could be an application, such as "gopls" in the case
|
||||
// of "gopls/client:vscode", or a shared library, such as "crash" in the
|
||||
// case of the "crash/crash" counter owned by the crashmonitor library. If
|
||||
// the entity name itself contains a '/', that's ok: "cmd/go/flag" is fine.
|
||||
//
|
||||
// - Words should be '-' separated, as in "gopls/completion/errors-latency"
|
||||
//
|
||||
// - Histograms should use bucket names identifying upper bounds with '<'.
|
||||
// For example given two counters "gopls/completion/latency:<50ms" and
|
||||
// "gopls/completion/latency:<100ms", the "<100ms" bucket counts events
|
||||
// with latency in the half-open interval [50ms, 100ms).
|
||||
//
|
||||
// # Debugging
|
||||
//
|
||||
// The GODEBUG environment variable can enable printing of additional debug
|
||||
// information for counters. Adding GODEBUG=countertrace=1 to the environment
|
||||
// of a process using counters causes the x/telemetry/counter package to log
|
||||
// counter information to stderr.
|
||||
package counter
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package crashmonitor
|
||||
|
||||
import ic "golang.org/x/telemetry/internal/crashmonitor"
|
||||
|
||||
// Supported reports whether the runtime supports [runtime.SetCrashOutput].
|
||||
//
|
||||
// TODO(adonovan): eliminate once go1.23+ is assured.
|
||||
func Supported() bool { return ic.Supported() }
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package telemetry
|
||||
|
||||
import "golang.org/x/telemetry/internal/telemetry"
|
||||
|
||||
// Dir returns the telemetry directory.
|
||||
func Dir() string {
|
||||
return telemetry.Default.Dir()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package telemetry
|
||||
@@ -0,0 +1,10 @@
|
||||
module golang.org/x/telemetry
|
||||
|
||||
go 1.20
|
||||
|
||||
require golang.org/x/mod v0.20.0
|
||||
|
||||
require (
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.23.0
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The chartconfig package defines the ChartConfig type, representing telemetry
|
||||
// chart configuration, as well as utilities for parsing and validating this
|
||||
// configuration.
|
||||
//
|
||||
// Chart configuration defines the set of aggregations active on the telemetry
|
||||
// server, and are used to derive which data needs to be uploaded by users.
|
||||
// See the original blog post for more details:
|
||||
//
|
||||
// https://research.swtch.com/telemetry-design#configuration
|
||||
//
|
||||
// The record format defined in this package differs slightly from that of the
|
||||
// blog post. This format is still experimental, and subject to change.
|
||||
//
|
||||
// Configuration records consist of fields, comments, and whitespace. A field
|
||||
// is defined by a line starting with a valid key, followed immediately by ":",
|
||||
// and then a textual value, which cannot include the comment separator '#'.
|
||||
//
|
||||
// Comments start with '#', and extend to the end of the line.
|
||||
//
|
||||
// The following keys are supported. Any entry not marked as (optional) must be
|
||||
// provided.
|
||||
//
|
||||
// - title: the chart title.
|
||||
// - description: (optional) a longer description of the chart.
|
||||
// - issue: a go issue tracker URL proposing the chart configuration.
|
||||
// Multiple issues may be provided by including additional 'issue:' lines.
|
||||
// All proposals must be in the 'accepted' state.
|
||||
// - type: the chart type: currently only partition, histogram, and stack are
|
||||
// supported.
|
||||
// - program: the package path of the program for which this chart applies.
|
||||
// - version: (optional) the first version for which this chart applies. Must
|
||||
// be a valid semver value.
|
||||
// - counter: the primary counter this chart illustrates, including buckets
|
||||
// for histogram and partition charts.
|
||||
// - depth: (optional) stack counters only; the maximum stack depth to collect
|
||||
// - error: (optional) the desired error rate for this chart, which
|
||||
// determines collection rate
|
||||
//
|
||||
// Multiple records are separated by "---" lines.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// # This config defines an ordinary counter.
|
||||
// counter: gopls/editor:{emacs,vim,vscode,other} # TODO(golang/go#34567): add more editors
|
||||
// title: Editor Distribution
|
||||
// description: measure editor distribution for gopls users.
|
||||
// type: partition
|
||||
// issue: https://go.dev/issue/12345
|
||||
// program: golang.org/x/tools/gopls
|
||||
// version: v1.0.0
|
||||
// version: [v2.0.0, v2.3.4]
|
||||
// version: [v3.0.0, ]
|
||||
//
|
||||
// ---
|
||||
//
|
||||
// # This config defines a stack counter.
|
||||
// counter: gopls/bug
|
||||
// title: Gopls bug reports.
|
||||
// description: Stacks of bugs encountered on the gopls server.
|
||||
// issue: https://go.dev/12345
|
||||
// issue: https://go.dev/23456 # increase stack depth
|
||||
// type: stack
|
||||
// program: golang.org/x/tools/gopls
|
||||
// depth: 10
|
||||
package chartconfig
|
||||
|
||||
// A ChartConfig defines the configuration for a single chart/collection on the
|
||||
// telemetry server.
|
||||
//
|
||||
// See the package documentation for field definitions.
|
||||
type ChartConfig struct {
|
||||
Title string
|
||||
Description string
|
||||
Issue []string
|
||||
Type string
|
||||
Program string
|
||||
Counter string
|
||||
Depth int
|
||||
Error float64 // TODO(rfindley) is Error still useful?
|
||||
Version string
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
# Note: these are approved chart configs, used to generate the upload config.
|
||||
# For the chart config file format, see chartconfig.go.
|
||||
|
||||
title: Editor Distribution
|
||||
counter: gopls/client:{vscode,vscodium,vscode-insiders,code-server,eglot,govim,neovim,coc.nvim,sublimetext,other}
|
||||
description: measure editor distribution for gopls users.
|
||||
type: partition
|
||||
issue: https://go.dev/issue/61038
|
||||
issue: https://go.dev/issue/62214 # add vscode-insiders
|
||||
program: golang.org/x/tools/gopls
|
||||
version: v0.13.0 # temporarily back-version to demonstrate config generation.
|
||||
---
|
||||
title: Go versions in use for gopls views
|
||||
counter: gopls/goversion:{1.16,1.17,1.18,1.19,1.20,1.21,1.22,1.23,1.24,1.25,1.26,1.27,1.28,1.29,1.30}
|
||||
description: measure go version usage distribution.
|
||||
type: partition
|
||||
issue: https://go.dev/issue/62248
|
||||
program: golang.org/x/tools/gopls
|
||||
version: v0.13.0
|
||||
---
|
||||
title: Number of bug report calls
|
||||
counter: gopls/bug
|
||||
description: count the bugs reported through gopls/internal/bug APIs.
|
||||
type: stack
|
||||
issue: https://go.dev/issue/62249
|
||||
program: golang.org/x/tools/gopls
|
||||
depth: 16
|
||||
version: v0.13.0
|
||||
---
|
||||
counter: crash/crash
|
||||
title: Unexpected Go crashes
|
||||
description: stacks of goroutines running when the Go program crashed
|
||||
type: stack
|
||||
issue: https://go.dev/issue/65696
|
||||
program: golang.org/x/tools/gopls
|
||||
depth: 16
|
||||
version: v0.15.0
|
||||
---
|
||||
counter: crash/malformed
|
||||
title: Failure to parse runtime crash output
|
||||
description: count of runtime crash messages that failed to parse
|
||||
type: partition
|
||||
issue: https://go.dev/issue/65696
|
||||
program: golang.org/x/tools/gopls
|
||||
version: v0.15.0
|
||||
---
|
||||
counter: crash/no-running-goroutine
|
||||
title: Failure to identify any running goroutine in the crash output
|
||||
description: count of runtime crash messages that don't have a running goroutine (e.g. deadlock)
|
||||
type: partition
|
||||
issue: https://go.dev/issue/65696
|
||||
program: golang.org/x/tools/gopls
|
||||
version: v0.15.0
|
||||
---
|
||||
counter: go/invocations
|
||||
title: cmd/go invocations
|
||||
description: Number of invocations of the go command
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67244
|
||||
program: cmd/go
|
||||
version: go1.23rc1
|
||||
---
|
||||
counter: go/build/flag:{
|
||||
buildmode
|
||||
}
|
||||
title: cmd/go flags
|
||||
description: Flag names of flags provided to the go command
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67244
|
||||
program: cmd/go
|
||||
version: go1.23rc1
|
||||
---
|
||||
counter: go/build/flag/buildmode:{
|
||||
archive,
|
||||
c-archive,
|
||||
c-shared,
|
||||
default,
|
||||
exe,
|
||||
pie,
|
||||
shared,
|
||||
plugin
|
||||
}
|
||||
title: cmd/go buildmode values
|
||||
description: Buildmode values for the go command
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67244
|
||||
program: cmd/go
|
||||
version: go1.23rc1
|
||||
---
|
||||
counter: compile/invocations
|
||||
title: cmd/compile invocations
|
||||
description: Number of invocations of the go compiler
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67244
|
||||
program: cmd/compile
|
||||
version: go1.23rc1
|
||||
---
|
||||
title: Compiler bug report calls
|
||||
counter: compile/bug
|
||||
description: count stacks for cases where cmd/compile has a fatal error
|
||||
type: stack
|
||||
issue: https://go.dev/issue/67244
|
||||
program: cmd/compile
|
||||
depth: 16
|
||||
version: go1.23rc1
|
||||
---
|
||||
counter: govulncheck/scan:{symbol,package,module}
|
||||
title: Scan Level Distribution
|
||||
description: measure govulncheck scan level distribution
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67678
|
||||
program: golang.org/x/vuln/cmd/govulncheck
|
||||
---
|
||||
counter: govulncheck/mode:{source,binary,extract,query,convert}
|
||||
title: Scan Mode Distribution
|
||||
description: measure govulncheck scan mode distribution
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67678
|
||||
program: golang.org/x/vuln/cmd/govulncheck
|
||||
---
|
||||
counter: govulncheck/format:{text,json,sarif,openvex}
|
||||
title: Output Format Distribution
|
||||
description: measure govulncheck output format distribution
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67678
|
||||
program: golang.org/x/vuln/cmd/govulncheck
|
||||
---
|
||||
counter: govulncheck/show:{none,traces,color,verbose,version}
|
||||
title: Show Options Distribution
|
||||
description: measure govulncheck show flag distribution
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67678
|
||||
program: golang.org/x/vuln/cmd/govulncheck
|
||||
---
|
||||
counter: govulncheck/assumptions:{multi-patterns,no-binary-platform,no-relative-path,no-go-root,local-replace,unknown-pkg-mod-path}
|
||||
title: Code Invariants Distribution
|
||||
description: measure distribution of failed govulncheck internal assumptions
|
||||
type: partition
|
||||
issue: https://go.dev/issue/67678
|
||||
program: golang.org/x/vuln/cmd/govulncheck
|
||||
---
|
||||
counter: gopls/gotoolchain:{auto,path,local,other}
|
||||
title: GOTOOLCHAIN types used with gopls
|
||||
description: measure the types of GOTOOLCHAIN values used with gopls
|
||||
type: partition
|
||||
issue: https://go.dev/issue/68771
|
||||
program: golang.org/x/tools/gopls
|
||||
version: v0.16.0
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package chartconfig
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
//go:embed config.txt
|
||||
var chartConfig []byte
|
||||
|
||||
func Raw() []byte {
|
||||
return chartConfig
|
||||
}
|
||||
|
||||
// Load loads and parses the current chart config.
|
||||
func Load() ([]ChartConfig, error) {
|
||||
return Parse(chartConfig)
|
||||
}
|
||||
|
||||
// Parse parses ChartConfig records from the provided raw data, returning an
|
||||
// error if the config has invalid syntax. See the package documentation for a
|
||||
// description of the record syntax.
|
||||
//
|
||||
// Even with correct syntax, the resulting chart config may not meet all the
|
||||
// requirements described in the package doc. Call [Validate] to check whether
|
||||
// the config data is coherent.
|
||||
func Parse(data []byte) ([]ChartConfig, error) {
|
||||
// Collect field information for the record type.
|
||||
var (
|
||||
prefixes []string // for parse errors
|
||||
fields = make(map[string]reflect.StructField) // key -> struct field
|
||||
)
|
||||
{
|
||||
typ := reflect.TypeOf(ChartConfig{})
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
f := typ.Field(i)
|
||||
key := strings.ToLower(f.Name)
|
||||
if _, ok := fieldParsers[key]; !ok {
|
||||
panic(fmt.Sprintf("no parser for field %q", f.Name))
|
||||
}
|
||||
prefixes = append(prefixes, "'"+key+":'")
|
||||
fields[key] = f
|
||||
}
|
||||
sort.Strings(prefixes)
|
||||
}
|
||||
|
||||
// Read records, separated by '---'
|
||||
var (
|
||||
records []ChartConfig
|
||||
inProgress = new(ChartConfig) // record value currently being parsed
|
||||
set = make(map[string]bool) // fields that are set so far; empty records are skipped
|
||||
)
|
||||
flushRecord := func() {
|
||||
if len(set) > 0 { // only flush non-empty records
|
||||
records = append(records, *inProgress)
|
||||
}
|
||||
inProgress = new(ChartConfig)
|
||||
set = make(map[string]bool)
|
||||
}
|
||||
|
||||
// Within bucket braces in counter fields, newlines are ignored.
|
||||
// if we're in the middle of a multiline counter field, accumulatedCounterText
|
||||
// contains the joined lines of the field up to the current line. Once
|
||||
// a line containing an end brace is reached, line will be set to the
|
||||
// joined lines of accumulatedCounterText and processed as a single line.
|
||||
var accumulatedCounterText string
|
||||
|
||||
for lineNum, line := range strings.Split(string(data), "\n") {
|
||||
if line == "---" {
|
||||
if accumulatedCounterText != "" {
|
||||
return nil, fmt.Errorf("line %d: reached end of record while processing multiline counter field", lineNum)
|
||||
}
|
||||
flushRecord()
|
||||
continue
|
||||
}
|
||||
text, _, _ := strings.Cut(line, "#") // trim comments
|
||||
|
||||
// Processing of counter fields which can appear across multiple lines.
|
||||
// See comment on accumulatedCounterText.
|
||||
if accumulatedCounterText == "" {
|
||||
if oi := strings.Index(text, "{"); oi >= 0 {
|
||||
if strings.Contains(text[:oi], "}") {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
|
||||
}
|
||||
if strings.Contains(text[oi+len("{"):], "{") {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '{'", lineNum, line)
|
||||
}
|
||||
if !strings.HasPrefix(text, "counter:") {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear within a counter field", lineNum, line)
|
||||
}
|
||||
accumulatedCounterText = strings.TrimRightFunc(text, unicode.IsSpace)
|
||||
// Don't continue here. If the counter field is a single line
|
||||
// the check for the close brace below will close the line
|
||||
// and process it as text. Set text to "" so when it's appended to
|
||||
// accumulatedCounterText we don't add the line twice.
|
||||
text = ""
|
||||
} else if strings.Contains(text, "}") {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
|
||||
}
|
||||
}
|
||||
if accumulatedCounterText != "" {
|
||||
if strings.Contains(text, "{") {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: '{' is only allowed to appear once within a counter field", lineNum, line)
|
||||
}
|
||||
accumulatedCounterText += strings.TrimSpace(text)
|
||||
if ci := strings.Index(accumulatedCounterText, "}"); ci >= 0 {
|
||||
if strings.Contains(accumulatedCounterText[ci+len("}"):], "}") {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}'", lineNum, line)
|
||||
}
|
||||
if ci > 0 && strings.HasSuffix(accumulatedCounterText[:ci], ",") {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: unexpected '}' after ','", lineNum, line)
|
||||
}
|
||||
text = accumulatedCounterText
|
||||
accumulatedCounterText = ""
|
||||
} else {
|
||||
// We're in the middle of a multiline counter field. Continue
|
||||
// processing.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var key string
|
||||
for k := range fields {
|
||||
prefix := k + ":"
|
||||
if strings.HasPrefix(text, prefix) {
|
||||
key = k
|
||||
text = text[len(prefix):]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" {
|
||||
// Check for empty lines before the field == nil check below.
|
||||
// Lines consisting only of whitespace and comments are OK.
|
||||
continue
|
||||
}
|
||||
if key == "" {
|
||||
return nil, fmt.Errorf("line %d: invalid line %q: lines must be '---', consist only of whitespace/comments, or start with %s", lineNum, line, strings.Join(prefixes, ", "))
|
||||
}
|
||||
field := fields[key]
|
||||
v := reflect.ValueOf(inProgress).Elem().FieldByName(field.Name)
|
||||
if set[key] && field.Type.Kind() != reflect.Slice {
|
||||
return nil, fmt.Errorf("line %d: field %s may not be repeated", lineNum, strings.ToLower(field.Name))
|
||||
}
|
||||
parser := fieldParsers[key]
|
||||
if err := parser(v, text); err != nil {
|
||||
return nil, fmt.Errorf("line %d: field %q: %v", lineNum, field.Name, err)
|
||||
}
|
||||
set[key] = true
|
||||
}
|
||||
|
||||
if accumulatedCounterText != "" {
|
||||
return nil, fmt.Errorf("reached end of file while processing multiline counter field")
|
||||
}
|
||||
|
||||
flushRecord()
|
||||
return records, nil
|
||||
}
|
||||
|
||||
// A fieldParser parses the provided input and writes to v, which must be
|
||||
// addressable.
|
||||
type fieldParser func(v reflect.Value, input string) error
|
||||
|
||||
var fieldParsers = map[string]fieldParser{
|
||||
"title": parseString,
|
||||
"description": parseString,
|
||||
"issue": parseSlice(parseString),
|
||||
"type": parseString,
|
||||
"program": parseString,
|
||||
"counter": parseString,
|
||||
"depth": parseInt,
|
||||
"error": parseFloat,
|
||||
"version": parseString,
|
||||
}
|
||||
|
||||
func parseString(v reflect.Value, input string) error {
|
||||
v.SetString(input)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseInt(v reflect.Value, input string) error {
|
||||
i, err := strconv.ParseInt(input, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid int value %q", input)
|
||||
}
|
||||
v.SetInt(i)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseFloat(v reflect.Value, input string) error {
|
||||
f, err := strconv.ParseFloat(input, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid float value %q", input)
|
||||
}
|
||||
v.SetFloat(f)
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSlice(elemParser fieldParser) fieldParser {
|
||||
return func(v reflect.Value, input string) error {
|
||||
elem := reflect.New(v.Type().Elem()).Elem()
|
||||
v.Set(reflect.Append(v, elem))
|
||||
elem = v.Index(v.Len() - 1)
|
||||
if err := elemParser(elem, input); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package chartconfig_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/internal/chartconfig"
|
||||
)
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
// Test that we can actually load the chart config.
|
||||
if _, err := chartconfig.Load(); err != nil {
|
||||
t.Errorf("Load() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
want []chartconfig.ChartConfig
|
||||
}{
|
||||
{"empty", "", nil},
|
||||
{"single field", "title: A", []chartconfig.ChartConfig{{Title: "A"}}},
|
||||
{
|
||||
"basic", `
|
||||
title: A
|
||||
description: B
|
||||
type: C
|
||||
program: D
|
||||
counter: E
|
||||
issue: F1
|
||||
issue: F2
|
||||
depth: 2
|
||||
error: 0.1
|
||||
version: v2.0.0
|
||||
`,
|
||||
[]chartconfig.ChartConfig{{
|
||||
Title: "A",
|
||||
Description: "B",
|
||||
Type: "C",
|
||||
Program: "D",
|
||||
Counter: "E",
|
||||
Issue: []string{"F1", "F2"},
|
||||
Depth: 2,
|
||||
Error: 0.1,
|
||||
Version: "v2.0.0",
|
||||
}},
|
||||
},
|
||||
{
|
||||
"partial", `
|
||||
title: A
|
||||
description: B
|
||||
`,
|
||||
[]chartconfig.ChartConfig{
|
||||
{Title: "A", Description: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"comments and whitespace", `
|
||||
# A comment
|
||||
title: A # a line comment
|
||||
|
||||
# Another comment
|
||||
|
||||
description: B
|
||||
|
||||
|
||||
`,
|
||||
[]chartconfig.ChartConfig{
|
||||
{Title: "A", Description: "B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"multi-record", `
|
||||
# Empty records are skipped
|
||||
---
|
||||
title: A
|
||||
description: B
|
||||
|
||||
---
|
||||
|
||||
title: C
|
||||
description: D
|
||||
`,
|
||||
[]chartconfig.ChartConfig{
|
||||
{Title: "A", Description: "B"},
|
||||
{Title: "C", Description: "D"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"example", `
|
||||
title: Editor Distribution
|
||||
counter: gopls/editor:{emacs,vim,vscode,other}
|
||||
description: measure editor distribution for gopls users.
|
||||
type: partition
|
||||
issue: TBD
|
||||
program: golang.org/x/tools/gopls
|
||||
`,
|
||||
[]chartconfig.ChartConfig{
|
||||
{
|
||||
Title: "Editor Distribution",
|
||||
Description: "measure editor distribution for gopls users.",
|
||||
Counter: "gopls/editor:{emacs,vim,vscode,other}",
|
||||
Type: "partition",
|
||||
Issue: []string{"TBD"},
|
||||
Program: "golang.org/x/tools/gopls",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiline counter field", `
|
||||
counter: foo:{
|
||||
bar,
|
||||
baz
|
||||
}
|
||||
`,
|
||||
[]chartconfig.ChartConfig{
|
||||
{Counter: "foo:{bar,baz}"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiline counter field with braces immediately next to text", `
|
||||
counter: foo:{bar,
|
||||
baz}
|
||||
`,
|
||||
[]chartconfig.ChartConfig{
|
||||
{Counter: "foo:{bar,baz}"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := chartconfig.Parse([]byte(test.input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse(...) failed: %v", err)
|
||||
}
|
||||
if len(got) != len(test.want) {
|
||||
t.Fatalf("Parse(...) returned %d records, want %d", len(got), len(test.want))
|
||||
}
|
||||
for i, got := range got {
|
||||
want := test.want[i]
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("Parse(...): record %d = %#v, want %#v", i, got, want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
"leading space",
|
||||
`
|
||||
title: foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
"unknown key",
|
||||
`
|
||||
foo: bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
"bad separator",
|
||||
`
|
||||
title: foo
|
||||
--- # comments aren't allowed after separators
|
||||
title: bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
"invalid depth",
|
||||
`
|
||||
depth: notanint
|
||||
`,
|
||||
},
|
||||
{
|
||||
"open curly brace not in counter field",
|
||||
`
|
||||
title: {
|
||||
`,
|
||||
},
|
||||
{
|
||||
"close curly brace not in counter field",
|
||||
`
|
||||
title: }
|
||||
`,
|
||||
},
|
||||
{
|
||||
"end of record within multiline counter field",
|
||||
`
|
||||
counter: foo{
|
||||
bar
|
||||
---
|
||||
title: baz
|
||||
`,
|
||||
},
|
||||
{
|
||||
"end of file within multiline counter field",
|
||||
`
|
||||
counter: foo{
|
||||
bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
"close curly before open curly",
|
||||
`
|
||||
counter: }foo{
|
||||
bar
|
||||
}`,
|
||||
},
|
||||
{
|
||||
"open curly after close curly",
|
||||
`
|
||||
counter: foo{
|
||||
bar
|
||||
} {`,
|
||||
},
|
||||
{
|
||||
"open curly after open curly same line",
|
||||
`
|
||||
counter: foo{{
|
||||
bar
|
||||
}`,
|
||||
},
|
||||
{
|
||||
"open curly after open curly different line",
|
||||
`
|
||||
counter: foo{
|
||||
{bar
|
||||
}`,
|
||||
},
|
||||
{
|
||||
"close curly after close curly",
|
||||
`
|
||||
counter: foo{
|
||||
bar
|
||||
} }`,
|
||||
},
|
||||
{
|
||||
"comma right before close curly",
|
||||
`
|
||||
counter: foo{
|
||||
bar,
|
||||
baz,
|
||||
} }`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
_, err := chartconfig.Parse([]byte(test.input))
|
||||
if err == nil {
|
||||
t.Fatalf("Parse(...) succeeded unexpectedly")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// package config provides methods for loading and querying a
|
||||
// telemetry upload config file.
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
// Config is a wrapper around telemetry.UploadConfig that provides some
|
||||
// convenience methods for checking the contents of a report.
|
||||
type Config struct {
|
||||
*telemetry.UploadConfig
|
||||
program map[string]bool
|
||||
goos map[string]bool
|
||||
goarch map[string]bool
|
||||
goversion map[string]bool
|
||||
pgversion map[pgkey]bool
|
||||
pgcounter map[pgkey]bool
|
||||
pgcounterprefix map[pgkey]bool
|
||||
pgstack map[pgkey]bool
|
||||
rate map[pgkey]float64
|
||||
}
|
||||
|
||||
type pgkey struct {
|
||||
program, key string
|
||||
}
|
||||
|
||||
func ReadConfig(file string) (*Config, error) {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg telemetry.UploadConfig
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewConfig(&cfg), nil
|
||||
}
|
||||
|
||||
func NewConfig(cfg *telemetry.UploadConfig) *Config {
|
||||
ucfg := Config{UploadConfig: cfg}
|
||||
ucfg.goos = set(ucfg.GOOS)
|
||||
ucfg.goarch = set(ucfg.GOARCH)
|
||||
ucfg.goversion = set(ucfg.GoVersion)
|
||||
ucfg.program = make(map[string]bool, len(ucfg.Programs))
|
||||
ucfg.pgversion = make(map[pgkey]bool, len(ucfg.Programs))
|
||||
ucfg.pgcounter = make(map[pgkey]bool, len(ucfg.Programs))
|
||||
ucfg.pgcounterprefix = make(map[pgkey]bool, len(ucfg.Programs))
|
||||
ucfg.pgstack = make(map[pgkey]bool, len(ucfg.Programs))
|
||||
ucfg.rate = make(map[pgkey]float64)
|
||||
for _, p := range ucfg.Programs {
|
||||
ucfg.program[p.Name] = true
|
||||
for _, v := range p.Versions {
|
||||
ucfg.pgversion[pgkey{p.Name, v}] = true
|
||||
}
|
||||
for _, c := range p.Counters {
|
||||
for _, e := range Expand(c.Name) {
|
||||
ucfg.pgcounter[pgkey{p.Name, e}] = true
|
||||
ucfg.rate[pgkey{p.Name, e}] = c.Rate
|
||||
}
|
||||
prefix, _, found := strings.Cut(c.Name, ":")
|
||||
if found {
|
||||
ucfg.pgcounterprefix[pgkey{p.Name, prefix}] = true
|
||||
}
|
||||
}
|
||||
for _, s := range p.Stacks {
|
||||
ucfg.pgstack[pgkey{p.Name, s.Name}] = true
|
||||
ucfg.rate[pgkey{p.Name, s.Name}] = s.Rate
|
||||
}
|
||||
}
|
||||
return &ucfg
|
||||
}
|
||||
|
||||
func (r *Config) HasProgram(s string) bool {
|
||||
return r.program[s]
|
||||
}
|
||||
|
||||
func (r *Config) HasGOOS(s string) bool {
|
||||
return r.goos[s]
|
||||
}
|
||||
|
||||
func (r *Config) HasGOARCH(s string) bool {
|
||||
return r.goarch[s]
|
||||
}
|
||||
|
||||
func (r *Config) HasGoVersion(s string) bool {
|
||||
return r.goversion[s]
|
||||
}
|
||||
|
||||
func (r *Config) HasVersion(program, version string) bool {
|
||||
return r.pgversion[pgkey{program, version}]
|
||||
}
|
||||
|
||||
func (r *Config) HasCounter(program, counter string) bool {
|
||||
return r.pgcounter[pgkey{program, counter}]
|
||||
}
|
||||
|
||||
func (r *Config) HasCounterPrefix(program, prefix string) bool {
|
||||
return r.pgcounterprefix[pgkey{program, prefix}]
|
||||
}
|
||||
|
||||
func (r *Config) HasStack(program, stack string) bool {
|
||||
return r.pgstack[pgkey{program, stack}]
|
||||
}
|
||||
|
||||
func (r *Config) Rate(program, name string) float64 {
|
||||
return r.rate[pgkey{program, name}]
|
||||
}
|
||||
|
||||
func set(slice []string) map[string]bool {
|
||||
s := make(map[string]bool, len(slice))
|
||||
for _, v := range slice {
|
||||
s[v] = true
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Expand takes a counter defined with buckets and expands it into distinct
|
||||
// strings for each bucket
|
||||
func Expand(counter string) []string {
|
||||
prefix, rest, hasBuckets := strings.Cut(counter, "{")
|
||||
var counters []string
|
||||
if hasBuckets {
|
||||
buckets := strings.Split(strings.TrimSuffix(rest, "}"), ",")
|
||||
for _, b := range buckets {
|
||||
counters = append(counters, prefix+b)
|
||||
}
|
||||
} else {
|
||||
counters = append(counters, prefix)
|
||||
}
|
||||
return counters
|
||||
}
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
f, err := os.Open(filepath.FromSlash("../../config/config.json"))
|
||||
if os.IsNotExist(err) {
|
||||
t.Skip("config file not found")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
var cfg telemetry.UploadConfig
|
||||
d := json.NewDecoder(f)
|
||||
d.DisallowUnknownFields()
|
||||
if err := d.Decode(&cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalConfig(t *testing.T) {
|
||||
got, err := ReadConfig("testdata/config.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantGOOS := []string{"linux", "darwin"}
|
||||
wantGOARCH := []string{"amd64", "arm64"}
|
||||
wantGoVersion := []string{"go1.20", "go1.20.1"}
|
||||
wantPrograms := []string{"golang.org/x/tools/gopls", "cmd/go"}
|
||||
wantVersions := [][2]string{
|
||||
{"golang.org/x/tools/gopls", "v0.10.1"},
|
||||
{"golang.org/x/tools/gopls", "v0.11.0"},
|
||||
}
|
||||
wantCounters := [][2]string{
|
||||
{"golang.org/x/tools/gopls", "editor:emacs"},
|
||||
{"golang.org/x/tools/gopls", "editor:vim"},
|
||||
{"golang.org/x/tools/gopls", "editor:vscode"},
|
||||
{"golang.org/x/tools/gopls", "editor:other"},
|
||||
{"cmd/go", "go/buildcache/miss:0"},
|
||||
{"cmd/go", "go/buildcache/miss:1"},
|
||||
{"cmd/go", "go/buildcache/miss:10"},
|
||||
{"cmd/go", "go/buildcache/miss:100"},
|
||||
}
|
||||
wantPrefix := [][2]string{
|
||||
{"golang.org/x/tools/gopls", "editor"},
|
||||
{"cmd/go", "go/buildcache/miss"},
|
||||
}
|
||||
|
||||
for _, w := range wantGOOS {
|
||||
if !got.HasGOOS(w) {
|
||||
t.Errorf("got.HasGOOS(%s) = false: want true", w)
|
||||
}
|
||||
}
|
||||
for _, w := range wantGOARCH {
|
||||
if !got.HasGOARCH(w) {
|
||||
t.Errorf("got.HasGOARCH(%s) = false: want true", w)
|
||||
}
|
||||
}
|
||||
for _, w := range wantGoVersion {
|
||||
if !got.HasGoVersion(w) {
|
||||
t.Errorf("got.HasGoVersion(%s) = false: want true", w)
|
||||
}
|
||||
}
|
||||
for _, w := range wantPrograms {
|
||||
if !got.HasProgram(w) {
|
||||
t.Errorf("got.HasProgram(%s) = false: want true", w)
|
||||
}
|
||||
}
|
||||
for _, w := range wantVersions {
|
||||
if !got.HasVersion(w[0], w[1]) {
|
||||
t.Errorf("got.HasVersion(%s, %s) = false: want true", w[0], w)
|
||||
}
|
||||
}
|
||||
for _, w := range wantCounters {
|
||||
if !got.HasCounter(w[0], w[1]) {
|
||||
t.Errorf("got.HasCounter(%s, %s) = false: want true", w[0], w[1])
|
||||
}
|
||||
}
|
||||
for _, w := range wantPrefix {
|
||||
if !got.HasCounterPrefix(w[0], w[1]) {
|
||||
t.Errorf("got.HasCounterPrefix(%s, %s) = false: want true", w[0], w[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"Version": "v0.0.1-test",
|
||||
"GOOS": [
|
||||
"linux",
|
||||
"darwin"
|
||||
],
|
||||
"GOARCH": [
|
||||
"amd64",
|
||||
"arm64"
|
||||
],
|
||||
"GoVersion": [
|
||||
"go1.20",
|
||||
"go1.20.1"
|
||||
],
|
||||
"Programs": [
|
||||
{
|
||||
"Name": "golang.org/x/tools/gopls",
|
||||
"Versions": [
|
||||
"v0.10.1",
|
||||
"v0.11.0"
|
||||
],
|
||||
"Counters": [
|
||||
{
|
||||
"Name": "editor:{emacs,vim,vscode,other}",
|
||||
"Rate": 0.01
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "cmd/go",
|
||||
"Versions": [
|
||||
"v0.10.1",
|
||||
"v0.11.0"
|
||||
],
|
||||
"Counters": [
|
||||
{
|
||||
"Name": "go/buildcache/miss:{0,0.1,0.2,0.5,1,10,100,2,20,5,50}",
|
||||
"Rate": 0.01
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+536
@@ -0,0 +1,536 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:generate go run . -w
|
||||
|
||||
//go:build go1.22
|
||||
|
||||
// Package configgen generates the upload config file stored in the config.json
|
||||
// file of golang.org/x/telemetry/config based on the chartconfig stored in
|
||||
// config.txt.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/version"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
"golang.org/x/telemetry/internal/chartconfig"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
var (
|
||||
write = flag.Bool("w", false, "if set, write the config file; otherwise, print to stdout")
|
||||
force = flag.Bool("f", false, "if set, force the write of the config file even if the current content is still valid")
|
||||
|
||||
// SamplingRate is the fraction of otherwise uploadable reports that will be uploaded
|
||||
SamplingRate = 1.0
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
gcfgs, err := chartconfig.Load()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// The padding heuristics below are based on the example of gopls.
|
||||
//
|
||||
// The goal is to pad enough versions for a quarter.
|
||||
uCfg, err := generate(gcfgs, padding{
|
||||
// 6 releases into the future translates to approximately three months for gopls.
|
||||
releases: 6,
|
||||
// We may release gopls 1.0, but won't release 2.0 in a three month timespan!
|
||||
maj: 1,
|
||||
// We don't usually do more than one minor release a month.
|
||||
majmin: 3,
|
||||
// Since golang/go#55267, which committed to adhering to semver, gopls
|
||||
// hasn't had more than 5 patches per minor version.
|
||||
patch: 6,
|
||||
// Gopls has never had more than 4 prereleases.
|
||||
pre: 4,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cfgJSON, err := json.MarshalIndent(uCfg, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !*write {
|
||||
fmt.Println(string(cfgJSON))
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
configFile, err := configFile()
|
||||
if err != nil {
|
||||
log.Fatalf("finding config file: %v", err)
|
||||
}
|
||||
|
||||
if !*force {
|
||||
currentCfg, err := readConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Guarantee that we have enough padding to do two patches releases tomorrow.
|
||||
minCfg, err := generate(gcfgs, padding{
|
||||
releases: 2,
|
||||
maj: 1,
|
||||
majmin: 1, // we're not ever going to do more than one major/minor release in a day
|
||||
patch: 2,
|
||||
pre: 2, // in a single day, we wouldn't prep more than two prereleases per version
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if contains(currentCfg, minCfg) {
|
||||
fmt.Fprintln(os.Stderr, "not writing the config file as it is still valid; use -f to force")
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(configFile, cfgJSON, 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// configFile returns the path to the x/telemetry/config config.json file in
|
||||
// this repo.
|
||||
//
|
||||
// The file must already exist: this won't be a valid location if running from
|
||||
// the module cache; this functionality only works when executed from the
|
||||
// telemetry repo.
|
||||
func configFile() (string, error) {
|
||||
out, err := exec.Command("go", "list", "-f", "{{.Dir}}", "golang.org/x/telemetry/internal/configgen").Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dir := strings.TrimSpace(string(out))
|
||||
configFile := filepath.Join(dir, "..", "..", "config", "config.json")
|
||||
if _, err := os.Stat(configFile); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
func readConfig(file string) (*telemetry.UploadConfig, error) {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config file: %v", err)
|
||||
}
|
||||
cfg := new(telemetry.UploadConfig)
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, fmt.Errorf("unmarshalling config file: %v", err)
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// generate computes the upload config from chart configs and module
|
||||
// information, returning the resulting formatted JSON.
|
||||
func generate(gcfgs []chartconfig.ChartConfig, padding padding) (*telemetry.UploadConfig, error) {
|
||||
ucfg := &telemetry.UploadConfig{
|
||||
GOOS: goos(),
|
||||
GOARCH: goarch(),
|
||||
// the probability of uploading a report
|
||||
SampleRate: SamplingRate,
|
||||
}
|
||||
var err error
|
||||
ucfg.GoVersion, err = goVersions()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("querying go info: %v", err)
|
||||
}
|
||||
|
||||
for i, r := range gcfgs {
|
||||
if err := ValidateChartConfig(r); err != nil {
|
||||
// TODO(rfindley): this is a poor way to identify the faulty record. We
|
||||
// should probably store position information in the ChartConfig.
|
||||
return nil, fmt.Errorf("chart config #%d (%q): %v", i, r.Title, err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
programs = make(map[string]*telemetry.ProgramConfig) // package path -> config
|
||||
minVersions = make(map[string]string) // package path -> min version required, or "" for all
|
||||
)
|
||||
for _, gcfg := range gcfgs {
|
||||
pcfg := programs[gcfg.Program]
|
||||
if pcfg == nil {
|
||||
pcfg = &telemetry.ProgramConfig{
|
||||
Name: gcfg.Program,
|
||||
}
|
||||
programs[gcfg.Program] = pcfg
|
||||
minVersions[gcfg.Program] = gcfg.Version
|
||||
}
|
||||
minVersions[gcfg.Program] = minVersion(minVersions[gcfg.Program], gcfg.Version)
|
||||
ccfg := telemetry.CounterConfig{
|
||||
Name: gcfg.Counter,
|
||||
Rate: 1.0, // TODO(rfindley): how should rate be configured?
|
||||
Depth: gcfg.Depth,
|
||||
}
|
||||
if gcfg.Depth > 0 {
|
||||
pcfg.Stacks = append(pcfg.Stacks, ccfg)
|
||||
} else {
|
||||
pcfg.Counters = append(pcfg.Counters, ccfg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range programs {
|
||||
minVersion := minVersions[p.Name]
|
||||
|
||||
// Collect eligible program versions. If p is a toolchain tool (cmd/go,
|
||||
// cmd/compile, etc), these come out of the Go versions queried above.
|
||||
// Otherwise, they come from the proxy.
|
||||
//
|
||||
// In both of these cases, the versions should be valid, but we verify
|
||||
// anyway as otherwise the version comparison is meaningless.
|
||||
// (and in fact, there is an invalid go1.9.2rc2 version in the proxy)
|
||||
if telemetry.IsToolchainProgram(p.Name) {
|
||||
// Note: no need to pad versions for toolchain programs, since the
|
||||
// toolchain is released infrequently.
|
||||
// (and in any case, version padding only works for semantic versions)
|
||||
for _, v := range ucfg.GoVersion {
|
||||
if !version.IsValid(v) {
|
||||
// The proxy toolchain versions list go1.9.2rc2, which is invalid.
|
||||
// Skip it.
|
||||
continue
|
||||
}
|
||||
|
||||
if minVersion == "" || version.Compare(minVersion, v) <= 0 {
|
||||
p.Versions = append(p.Versions, v)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
versions, err := listProxyVersions(p.Name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing versions for %q: %v", p.Name, err)
|
||||
}
|
||||
// Filter proxy versions in place.
|
||||
i := 0
|
||||
for _, v := range versions {
|
||||
if !semver.IsValid(v) {
|
||||
return nil, fmt.Errorf("invalid semver %q returned from proxy for %q", v, p.Name)
|
||||
}
|
||||
if minVersion == "" || semver.Compare(minVersion, v) <= 0 {
|
||||
versions[i] = v
|
||||
i++
|
||||
}
|
||||
}
|
||||
p.Versions = padVersions(versions[:i], prereleasesForProgram(p.Name), padding)
|
||||
}
|
||||
ucfg.Programs = append(ucfg.Programs, p)
|
||||
}
|
||||
sort.Slice(ucfg.Programs, func(i, j int) bool {
|
||||
return ucfg.Programs[i].Name < ucfg.Programs[j].Name
|
||||
})
|
||||
|
||||
return ucfg, nil
|
||||
}
|
||||
|
||||
// contains reports whether outer contains all program versions of inner, and
|
||||
// is otherwise equivalent to inner.
|
||||
func contains(outer, inner *telemetry.UploadConfig) bool {
|
||||
if !slices.Equal(outer.GOARCH, inner.GOARCH) {
|
||||
return false
|
||||
}
|
||||
if !slices.Equal(outer.GOOS, inner.GOOS) {
|
||||
return false
|
||||
}
|
||||
if !slices.Equal(outer.GoVersion, inner.GoVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, pi := range inner.Programs {
|
||||
i := slices.IndexFunc(outer.Programs, func(po *telemetry.ProgramConfig) bool {
|
||||
return po.Name == pi.Name
|
||||
})
|
||||
if i < 0 {
|
||||
return false
|
||||
}
|
||||
po := outer.Programs[i]
|
||||
if !sliceContains(po.Versions, pi.Versions) {
|
||||
return false
|
||||
}
|
||||
if !slices.Equal(po.Counters, pi.Counters) {
|
||||
return false
|
||||
}
|
||||
if !slices.Equal(po.Stacks, pi.Stacks) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for _, po := range outer.Programs {
|
||||
if !slices.ContainsFunc(inner.Programs, func(pi *telemetry.ProgramConfig) bool {
|
||||
return pi.Name == po.Name
|
||||
}) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sliceContains[T comparable](outer, inner []T) bool {
|
||||
m := toMap(outer)
|
||||
for _, v := range inner {
|
||||
if !m[v] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func toMap[T comparable](s []T) map[T]bool {
|
||||
m := make(map[T]bool)
|
||||
for _, v := range s {
|
||||
m[v] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// prereleasesForProgram returns the set of prereleases to use for the provided
|
||||
// program. We may need to customize this for the conventions of different
|
||||
// programs.
|
||||
func prereleasesForProgram(program string) []string {
|
||||
// Surely eight prereleases is enough for any program... :)
|
||||
return []string{"pre.1", "pre.2", "pre.3", "pre.4", "pre.5", "pre.6", "pre.7", "pre.8"}
|
||||
}
|
||||
|
||||
// minVersion returns the lesser semantic version of v1 and v2.
|
||||
//
|
||||
// As a special case, the empty string is treated as an absolute minimum
|
||||
// (empty => all versions are greater).
|
||||
func minVersion(v1, v2 string) string {
|
||||
if v1 == "" || v2 == "" {
|
||||
return ""
|
||||
}
|
||||
if semver.Compare(v1, v2) > 0 {
|
||||
return v2
|
||||
}
|
||||
return v1
|
||||
}
|
||||
|
||||
// goos returns a sorted slice of known GOOS values.
|
||||
func goos() []string {
|
||||
var gooses []string
|
||||
for goos := range knownOS {
|
||||
gooses = append(gooses, goos)
|
||||
}
|
||||
sort.Strings(gooses)
|
||||
return gooses
|
||||
}
|
||||
|
||||
// goarch returns a sorted slice of known GOARCH values.
|
||||
func goarch() []string {
|
||||
var arches []string
|
||||
for arch := range knownArch {
|
||||
arches = append(arches, arch)
|
||||
}
|
||||
sort.Strings(arches)
|
||||
return arches
|
||||
}
|
||||
|
||||
// goInfo queries the proxy for information about go distributions, including
|
||||
// versions, GOOS, and GOARCH values.
|
||||
func goVersions() ([]string, error) {
|
||||
// Trick: read Go distribution information from the module versions of
|
||||
// golang.org/toolchain. These define the set of valid toolchains, and
|
||||
// therefore are a reasonable source for version information.
|
||||
//
|
||||
// A more authoritative source for this information may be
|
||||
// https://go.dev/dl?mode=json&include=all.
|
||||
proxyVersions, err := listProxyVersions("golang.org/toolchain")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing toolchain versions: %v", err)
|
||||
}
|
||||
var goVersionRx = regexp.MustCompile(`^-(go.+)\.[^.]+-[^.]+$`)
|
||||
verSet := make(map[string]struct{})
|
||||
for _, v := range proxyVersions {
|
||||
pre := semver.Prerelease(v)
|
||||
match := goVersionRx.FindStringSubmatch(pre)
|
||||
if match == nil {
|
||||
return nil, fmt.Errorf("proxy version %q does not match prerelease regexp %q", v, goVersionRx)
|
||||
}
|
||||
verSet[match[1]] = struct{}{}
|
||||
}
|
||||
var vers []string
|
||||
for v := range verSet {
|
||||
vers = append(vers, v)
|
||||
}
|
||||
sort.Sort(byGoVersion(vers))
|
||||
return vers, nil
|
||||
}
|
||||
|
||||
type byGoVersion []string
|
||||
|
||||
func (vs byGoVersion) Len() int { return len(vs) }
|
||||
func (vs byGoVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
|
||||
func (vs byGoVersion) Less(i, j int) bool {
|
||||
cmp := version.Compare(vs[i], vs[j])
|
||||
if cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
// To ensure that we have a stable sort, order equivalent Go versions lexically.
|
||||
return vs[i] < vs[j]
|
||||
}
|
||||
|
||||
// versionsForTesting contains versions to use for testing, rather than
|
||||
// querying the proxy.
|
||||
var versionsForTesting map[string][]string
|
||||
|
||||
// listProxyVersions queries the Go module mirror for published versions of the
|
||||
// given modulePath.
|
||||
//
|
||||
// modulePath must be lower-case (or already escaped): this function doesn't do
|
||||
// any escaping of upper-cased letters, as is required by the proxy prototol
|
||||
// (https://go.dev/ref/mod#goproxy-protocol).
|
||||
func listProxyVersions(modulePath string) ([]string, error) {
|
||||
if vers, ok := versionsForTesting[modulePath]; ok {
|
||||
return vers, nil
|
||||
}
|
||||
cmd := exec.Command("go", "list", "-m", "--versions", modulePath)
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("listing versions: %v (stderr: %v)", err, stderr.String())
|
||||
}
|
||||
fields := strings.Fields(strings.TrimSpace(string(out)))
|
||||
if len(fields) == 0 {
|
||||
return nil, fmt.Errorf("invalid version list output: %q", string(out))
|
||||
}
|
||||
return fields[1:], nil
|
||||
}
|
||||
|
||||
// padding defines constraints on additional versions to pad.
|
||||
//
|
||||
// These constraints help restrict version padding to "reasonable" versions,
|
||||
// based on heuristics such as "we never do more than 3 prereleases for a given
|
||||
// version" or "we never have more than 5 patch versions" or "we can't do more
|
||||
// than 10 total releases over that time period". See the field documentation
|
||||
// for details.
|
||||
type padding struct {
|
||||
releases int // bounds on the total number of releases
|
||||
maj int // bounds the number of new major versions
|
||||
majmin int // bounds the number of new major+minor versions
|
||||
patch int // bounds the number of new patch versions
|
||||
pre int // the number of prereleases to pad per release
|
||||
}
|
||||
|
||||
// padVersions pads the existing version list with potential next versions, so
|
||||
// that we don't have to wait an additional day to start getting reports for a
|
||||
// newly tagged version.
|
||||
//
|
||||
// The prereleases argument may be supplied to provide a set of potential
|
||||
// prerelease candidates. For example, if the program releases prereleases of
|
||||
// the form "-pre.N", prereleases should be {"pre.1", "pre.2", ...}. For each
|
||||
// potential next release version, the next two prerelease versions will be
|
||||
// selected out of the provided set of prereleases.
|
||||
func padVersions(versions []string, prereleasePatterns []string, padding padding) []string {
|
||||
versions = slices.Clone(versions)
|
||||
semver.Sort(versions)
|
||||
|
||||
latestRelease := "v0.0.0"
|
||||
all := make(map[string]bool) // for de-duplicating padded versions
|
||||
for _, v := range versions {
|
||||
cv := semver.Canonical(v)
|
||||
all[cv] = true
|
||||
if semver.Prerelease(cv) == "" && semver.Compare(latestRelease, cv) < 0 {
|
||||
latestRelease = cv
|
||||
}
|
||||
}
|
||||
|
||||
parsedLatest, ok := parseSemver(latestRelease)
|
||||
if !ok {
|
||||
// "can't happen", since the latest release version should always be canonical.
|
||||
panic(fmt.Sprintf("unable to parse latest release version %q", latestRelease))
|
||||
}
|
||||
|
||||
// Pad the latest version only.
|
||||
//
|
||||
// This assumes that the program in question doesn't patch older releases
|
||||
// (as is the case with gopls). If that assumption ever changes, we may need
|
||||
// to apply padding to older versions as well.
|
||||
versionsToPad := []semversion{parsedLatest}
|
||||
|
||||
var maj, min, patch int
|
||||
for _, toPad := range versionsToPad {
|
||||
for majPadding := 0; majPadding <= padding.maj; majPadding++ {
|
||||
maj = toPad.major + majPadding
|
||||
for minPadding := 0; minPadding+majPadding <= padding.majmin; minPadding++ {
|
||||
if majPadding == 0 {
|
||||
min = toPad.minor + minPadding
|
||||
} else {
|
||||
min = minPadding
|
||||
}
|
||||
for patchPadding := 0; patchPadding <= padding.patch; patchPadding++ {
|
||||
releases := majPadding + minPadding + patchPadding
|
||||
if releases == 0 || releases > padding.releases {
|
||||
continue
|
||||
}
|
||||
if majPadding == 0 && minPadding == 0 {
|
||||
patch = toPad.patch + patchPadding
|
||||
} else {
|
||||
patch = patchPadding
|
||||
}
|
||||
|
||||
v := fmt.Sprintf("v%d.%d.%d", maj, min, patch)
|
||||
if all[v] {
|
||||
// This guard is future proofing: we may have seen this version
|
||||
// before if we are ever padding something other than the latest
|
||||
// version.
|
||||
continue
|
||||
}
|
||||
versions = append(versions, v)
|
||||
|
||||
// We may already have prereleases at this version. Don't pad
|
||||
// additional prereleases, under the assumption that we don't
|
||||
// typically have more than padding.pre prereleases.
|
||||
nextPrerelease := 0
|
||||
for i, patt := range prereleasePatterns {
|
||||
pre := fmt.Sprintf("%s-%s", v, patt)
|
||||
if all[pre] {
|
||||
nextPrerelease = i + 1
|
||||
}
|
||||
}
|
||||
for i := nextPrerelease; i < len(prereleasePatterns) && i < padding.pre; i++ {
|
||||
pre := fmt.Sprintf("%s-%s", v, prereleasePatterns[i])
|
||||
versions = append(versions, pre)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
semver.Sort(versions)
|
||||
return versions
|
||||
}
|
||||
|
||||
// version is a parsed semantic version.
|
||||
type semversion struct {
|
||||
major, minor, patch int
|
||||
pre string
|
||||
}
|
||||
|
||||
// parseSemver attempts to parse semver components out of the provided semver
|
||||
// v. If v is not valid semver in canonical form, parseSemver returns _, _, _,
|
||||
// _, false.
|
||||
func parseSemver(v string) (_ semversion, ok bool) {
|
||||
var parsed semversion
|
||||
v, parsed.pre, _ = strings.Cut(v, "-")
|
||||
if _, err := fmt.Sscanf(v, "v%d.%d.%d", &parsed.major, &parsed.minor, &parsed.patch); err == nil {
|
||||
ok = true
|
||||
}
|
||||
return parsed, ok
|
||||
}
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.22
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/internal/chartconfig"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
func TestGenerate(t *testing.T) {
|
||||
defer func(vers map[string][]string) {
|
||||
versionsForTesting = vers
|
||||
}(versionsForTesting)
|
||||
versionsForTesting = map[string][]string{
|
||||
"golang.org/toolchain": {"v0.0.1-go1.21.0.linux-arm", "v0.0.1-go1.20.linux-arm"},
|
||||
"golang.org/x/tools/gopls": {"v0.13.0", "v0.14.0", "v0.15.0-pre.1", "v0.15.0"},
|
||||
}
|
||||
const raw = `
|
||||
title: Editor Distribution
|
||||
counter: gopls/editor:{emacs,vim,vscode,other}
|
||||
description: measure editor distribution for gopls users.
|
||||
type: partition
|
||||
issue: https://go.dev/issue/61038
|
||||
program: golang.org/x/tools/gopls
|
||||
version: v0.14.0
|
||||
`
|
||||
gcfgs, err := chartconfig.Parse([]byte(raw))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := generate(gcfgs, padding{2, 1, 1, 2, 2})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := telemetry.UploadConfig{
|
||||
GOOS: goos(),
|
||||
GOARCH: goarch(),
|
||||
SampleRate: SamplingRate,
|
||||
GoVersion: []string{"go1.20", "go1.21.0"},
|
||||
Programs: []*telemetry.ProgramConfig{{
|
||||
Name: "golang.org/x/tools/gopls",
|
||||
Versions: []string{
|
||||
"v0.14.0",
|
||||
"v0.15.0-pre.1",
|
||||
"v0.15.0",
|
||||
"v0.15.1-pre.1",
|
||||
"v0.15.1-pre.2",
|
||||
"v0.15.1",
|
||||
"v0.15.2-pre.1",
|
||||
"v0.15.2-pre.2",
|
||||
"v0.15.2",
|
||||
"v0.16.0-pre.1",
|
||||
"v0.16.0-pre.2",
|
||||
"v0.16.0",
|
||||
"v0.16.1-pre.1",
|
||||
"v0.16.1-pre.2",
|
||||
"v0.16.1",
|
||||
"v1.0.0-pre.1",
|
||||
"v1.0.0-pre.2",
|
||||
"v1.0.0",
|
||||
"v1.0.1-pre.1",
|
||||
"v1.0.1-pre.2",
|
||||
"v1.0.1",
|
||||
},
|
||||
Counters: []telemetry.CounterConfig{{
|
||||
Name: "gopls/editor:{emacs,vim,vscode,other}",
|
||||
Rate: 1.0,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
if !reflect.DeepEqual(*got, want) {
|
||||
if len(got.Programs) != len(want.Programs) {
|
||||
t.Errorf("generate(): got %d programs, want %d", len(got.Programs), len(want.Programs))
|
||||
} else {
|
||||
for i, gotp := range got.Programs {
|
||||
want := *want.Programs[i]
|
||||
if !reflect.DeepEqual(*gotp, want) {
|
||||
t.Errorf("generate() program #%d =\n%+v\nwant:\n%+v", i, *gotp, want)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Errorf("generate() =\n%+v\nwant:\n%+v", *got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestByGoVersion_Less(t *testing.T) {
|
||||
got := []string{
|
||||
"go1.21.0",
|
||||
"go1.21rc1",
|
||||
"go1.9",
|
||||
"go1.9rc1",
|
||||
"go1.6",
|
||||
"go1.6beta1",
|
||||
}
|
||||
want := []string{
|
||||
"go1.6beta1",
|
||||
"go1.6",
|
||||
"go1.9rc1",
|
||||
"go1.9",
|
||||
"go1.21rc1",
|
||||
"go1.21.0",
|
||||
}
|
||||
sort.Sort(byGoVersion(got))
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("sort.Sort(byGoVersion(got)) = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContains(t *testing.T) {
|
||||
baseline := func() *telemetry.UploadConfig {
|
||||
return &telemetry.UploadConfig{
|
||||
GOOS: goos(),
|
||||
GOARCH: goarch(),
|
||||
GoVersion: []string{"go1.20", "go1.21.0"},
|
||||
Programs: []*telemetry.ProgramConfig{{
|
||||
Name: "golang.org/x/tools/gopls",
|
||||
Versions: []string{
|
||||
"v0.14.0",
|
||||
"v0.15.0-pre.1",
|
||||
"v0.15.0",
|
||||
"v0.15.1-pre.1",
|
||||
"v0.15.1-pre.2",
|
||||
"v0.15.1",
|
||||
},
|
||||
Counters: []telemetry.CounterConfig{{
|
||||
Name: "gopls/editor:{emacs,vim,vscode,other}",
|
||||
Rate: 1.0,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
outerMut, innerMut func(*telemetry.UploadConfig)
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"additional arch",
|
||||
func(cfg *telemetry.UploadConfig) { cfg.GOARCH = append(cfg.GOARCH, "fake") },
|
||||
func(cfg *telemetry.UploadConfig) {},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"additional program",
|
||||
func(cfg *telemetry.UploadConfig) { cfg.Programs = append(cfg.Programs, new(telemetry.ProgramConfig)) },
|
||||
func(cfg *telemetry.UploadConfig) {},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"additional counter",
|
||||
func(cfg *telemetry.UploadConfig) {
|
||||
cfg.Programs[0].Counters = append(cfg.Programs[0].Counters, telemetry.CounterConfig{})
|
||||
},
|
||||
func(cfg *telemetry.UploadConfig) {},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"additional version",
|
||||
func(cfg *telemetry.UploadConfig) {
|
||||
cfg.Programs[0].Versions = append(cfg.Programs[0].Versions, "v99.99.99")
|
||||
},
|
||||
func(cfg *telemetry.UploadConfig) {},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
outer := baseline()
|
||||
test.outerMut(outer)
|
||||
inner := baseline()
|
||||
test.innerMut(inner)
|
||||
if got := contains(outer, inner); got != test.want {
|
||||
t.Errorf("contains(...) = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPadVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
versions []string
|
||||
prereleasePatterns []string
|
||||
padding padding
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
nil,
|
||||
[]string{"pre.1"},
|
||||
padding{1, 1, 1, 1, 2},
|
||||
[]string{
|
||||
"v0.0.1-pre.1",
|
||||
"v0.0.1",
|
||||
"v0.1.0-pre.1",
|
||||
"v0.1.0",
|
||||
"v1.0.0-pre.1",
|
||||
"v1.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
[]string{"v0.8.3", "v0.9.1", "v0.9.2", "v1.0.0", "v1.0.1", "v1.0.2-pre.1", "v1.0.2-pre.2", "v1.0.2-pre.3"},
|
||||
[]string{"pre.1", "pre.2", "pre.3", "pre.4"},
|
||||
padding{2, 1, 2, 2, 2},
|
||||
[]string{
|
||||
"v0.8.3",
|
||||
"v0.9.1",
|
||||
"v0.9.2",
|
||||
"v1.0.0",
|
||||
"v1.0.1",
|
||||
"v1.0.2-pre.1",
|
||||
"v1.0.2-pre.2",
|
||||
"v1.0.2-pre.3",
|
||||
"v1.0.2",
|
||||
"v1.0.3-pre.1",
|
||||
"v1.0.3-pre.2",
|
||||
"v1.0.3",
|
||||
"v1.1.0-pre.1",
|
||||
"v1.1.0-pre.2",
|
||||
"v1.1.0",
|
||||
"v1.1.1-pre.1",
|
||||
"v1.1.1-pre.2",
|
||||
"v1.1.1",
|
||||
"v1.2.0-pre.1",
|
||||
"v1.2.0-pre.2",
|
||||
"v1.2.0",
|
||||
"v2.0.0-pre.1",
|
||||
"v2.0.0-pre.2",
|
||||
"v2.0.0",
|
||||
"v2.0.1-pre.1",
|
||||
"v2.0.1-pre.2",
|
||||
"v2.0.1",
|
||||
"v2.1.0-pre.1",
|
||||
"v2.1.0-pre.2",
|
||||
"v2.1.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := padVersions(test.versions, test.prereleasePatterns, test.padding)
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Errorf("padVersions(%v, %v) =\n%v\nwant:\n%v", test.versions, test.prereleasePatterns, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.22
|
||||
|
||||
package main
|
||||
|
||||
// knownOS is the list of past, present, and future known GOOS values.
|
||||
// Do not remove from this list, as it is used for filename matching.
|
||||
// If you add an entry to this list, look at unixOS, below.
|
||||
var knownOS = map[string]bool{
|
||||
"aix": true,
|
||||
"android": true,
|
||||
"darwin": true,
|
||||
"dragonfly": true,
|
||||
"freebsd": true,
|
||||
"hurd": true,
|
||||
"illumos": true,
|
||||
"ios": true,
|
||||
"js": true,
|
||||
"linux": true,
|
||||
"nacl": true,
|
||||
"netbsd": true,
|
||||
"openbsd": true,
|
||||
"plan9": true,
|
||||
"solaris": true,
|
||||
"wasip1": true,
|
||||
"windows": true,
|
||||
"zos": true,
|
||||
}
|
||||
|
||||
// knownArch is the list of past, present, and future known GOARCH values.
|
||||
// Do not remove from this list, as it is used for filename matching.
|
||||
var knownArch = map[string]bool{
|
||||
"386": true,
|
||||
"amd64": true,
|
||||
"amd64p32": true,
|
||||
"arm": true,
|
||||
"armbe": true,
|
||||
"arm64": true,
|
||||
"arm64be": true,
|
||||
"loong64": true,
|
||||
"mips": true,
|
||||
"mipsle": true,
|
||||
"mips64": true,
|
||||
"mips64le": true,
|
||||
"mips64p32": true,
|
||||
"mips64p32le": true,
|
||||
"ppc": true,
|
||||
"ppc64": true,
|
||||
"ppc64le": true,
|
||||
"riscv": true,
|
||||
"riscv64": true,
|
||||
"s390": true,
|
||||
"s390x": true,
|
||||
"sparc": true,
|
||||
"sparc64": true,
|
||||
"wasm": true,
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.22
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/version"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
"golang.org/x/telemetry/internal/chartconfig"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
// ValidateChartConfig checks that a ChartConfig is complete and coherent,
|
||||
// returning an error describing all problems encountered, or nil.
|
||||
func ValidateChartConfig(cfg chartconfig.ChartConfig) error {
|
||||
var errs []error
|
||||
reportf := func(format string, args ...any) {
|
||||
errs = append(errs, fmt.Errorf(format, args...))
|
||||
}
|
||||
if cfg.Title == "" {
|
||||
reportf("title must be set")
|
||||
}
|
||||
if len(cfg.Issue) == 0 {
|
||||
reportf("at least one issue is required")
|
||||
}
|
||||
if cfg.Program == "" {
|
||||
reportf("program must be set")
|
||||
}
|
||||
if cfg.Counter == "" {
|
||||
reportf("counter must be set")
|
||||
}
|
||||
if cfg.Type == "" {
|
||||
reportf("type must be set")
|
||||
}
|
||||
if cfg.Depth < 0 {
|
||||
reportf("invalid depth %d: must be non-negative", cfg.Depth)
|
||||
}
|
||||
if cfg.Depth != 0 && cfg.Type != "stack" {
|
||||
reportf("depth can only be set for \"stack\" chart types")
|
||||
}
|
||||
valid := semver.IsValid
|
||||
if telemetry.IsToolchainProgram(cfg.Program) {
|
||||
valid = version.IsValid
|
||||
}
|
||||
if cfg.Version != "" && !valid(cfg.Version) {
|
||||
reportf("%q is not a valid version (must be a go version or semver)", cfg.Version)
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.22
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/internal/chartconfig"
|
||||
)
|
||||
|
||||
func TestLoadedChartsAreValid(t *testing.T) {
|
||||
// Test that we can actually load the chart config.
|
||||
charts, err := chartconfig.Load()
|
||||
if err != nil {
|
||||
t.Errorf("Load() failed: %v", err)
|
||||
}
|
||||
for i, chart := range charts {
|
||||
if err := ValidateChartConfig(chart); err != nil {
|
||||
t.Errorf("Chart %d is invalid: %v", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateOK(t *testing.T) {
|
||||
// A minimally valid chart config.
|
||||
const input = `
|
||||
title: Editor Distribution
|
||||
counter: gopls/editor:{emacs,vim,vscode,other}
|
||||
type: partition
|
||||
issue: https://go.dev/issue/12345
|
||||
program: golang.org/x/tools/gopls
|
||||
`
|
||||
records, err := chartconfig.Parse([]byte(input))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(records) != 1 {
|
||||
t.Fatalf("Parse(%q) returned %d records, want exactly 1", input, len(records))
|
||||
}
|
||||
if err := ValidateChartConfig(records[0]); err != nil {
|
||||
t.Errorf("Validate(%q) = %v, want nil", input, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
tests := map[string][]string{ // input -> want errors
|
||||
// validation of mandatory fields
|
||||
"description:bar": {"title", "program", "issue", "counter", "type"},
|
||||
|
||||
// validation of semver intervals
|
||||
"version:1.2.3.4": {"semver"},
|
||||
|
||||
// valid of stack configuration
|
||||
"depth:-1": {"non-negative", "stack"},
|
||||
}
|
||||
|
||||
for input, wantErrs := range tests {
|
||||
records, err := chartconfig.Parse([]byte(input))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(records) != 1 {
|
||||
t.Fatalf("Parse(%q) returned %d records, want exactly 1", input, len(records))
|
||||
}
|
||||
err = ValidateChartConfig(records[0])
|
||||
if err == nil {
|
||||
t.Fatalf("Validate(%q) succeeded unexpectedly", input)
|
||||
}
|
||||
errs := err.Error()
|
||||
for _, want := range wantErrs {
|
||||
if !strings.Contains(errs, want) {
|
||||
t.Errorf("Validate(%q) = %v, want containing %q", input, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package configstore abstracts interaction with the telemetry config server.
|
||||
// Telemetry config (golang.org/x/telemetry/config) is distributed as a go
|
||||
// module containing go.mod and config.json. Programs that upload collected
|
||||
// counters download the latest config using `go mod download`. This provides
|
||||
// verification of downloaded configuration and cacheability.
|
||||
package configstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
const (
|
||||
ModulePath = "golang.org/x/telemetry/config"
|
||||
configFileName = "config.json"
|
||||
)
|
||||
|
||||
// needNoConsole is used on windows to set the windows.CREATE_NO_WINDOW
|
||||
// creation flag.
|
||||
var needNoConsole = func(cmd *exec.Cmd) {}
|
||||
|
||||
var downloads int64
|
||||
|
||||
// Downloads reports, for testing purposes, the number of times [Download] has
|
||||
// been called.
|
||||
func Downloads() int64 {
|
||||
return atomic.LoadInt64(&downloads)
|
||||
}
|
||||
|
||||
// Download fetches the requested telemetry UploadConfig using "go mod
|
||||
// download". If envOverlay is provided, it is appended to the environment used
|
||||
// for invoking the go command.
|
||||
//
|
||||
// The second result is the canonical version of the requested configuration.
|
||||
func Download(version string, envOverlay []string) (*telemetry.UploadConfig, string, error) {
|
||||
atomic.AddInt64(&downloads, 1)
|
||||
|
||||
if version == "" {
|
||||
version = "latest"
|
||||
}
|
||||
modVer := ModulePath + "@" + version
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.Command("go", "mod", "download", "-json", modVer)
|
||||
needNoConsole(cmd)
|
||||
cmd.Env = append(os.Environ(), envOverlay...)
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
var info struct {
|
||||
Error string
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &info); err == nil && info.Error != "" {
|
||||
return nil, "", fmt.Errorf("failed to download config module: %v", info.Error)
|
||||
}
|
||||
return nil, "", fmt.Errorf("failed to download config module: %w\n%s", err, &stderr)
|
||||
}
|
||||
|
||||
var info struct {
|
||||
Dir string
|
||||
Version string
|
||||
Error string
|
||||
}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &info); err != nil || info.Dir == "" {
|
||||
return nil, "", fmt.Errorf("failed to download config module (invalid JSON): %w", err)
|
||||
}
|
||||
data, err := os.ReadFile(filepath.Join(info.Dir, configFileName))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("invalid config module: %w", err)
|
||||
}
|
||||
cfg := new(telemetry.UploadConfig)
|
||||
if err := json.Unmarshal(data, cfg); err != nil {
|
||||
return nil, "", fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
return cfg, info.Version, nil
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package configstore_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/internal/configstore"
|
||||
"golang.org/x/telemetry/internal/configtest"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
"golang.org/x/telemetry/internal/testenv"
|
||||
)
|
||||
|
||||
func TestDownload(t *testing.T) {
|
||||
testenv.NeedsGo(t)
|
||||
|
||||
configVersion := "v0.1.0"
|
||||
in := &telemetry.UploadConfig{
|
||||
GOOS: []string{"darwin"},
|
||||
GOARCH: []string{"amd64", "arm64"},
|
||||
GoVersion: []string{"1.20.3", "1.20.4"},
|
||||
Programs: []*telemetry.ProgramConfig{{
|
||||
Name: "gopls",
|
||||
Versions: []string{"v0.11.0"},
|
||||
Counters: []telemetry.CounterConfig{{
|
||||
Name: "foobar",
|
||||
Rate: 2,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
env := configtest.LocalProxyEnv(t, in, configVersion)
|
||||
testCases := []struct {
|
||||
version string
|
||||
want telemetry.UploadConfig
|
||||
}{
|
||||
{version: configVersion, want: *in},
|
||||
{version: "latest", want: *in},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.version, func(t *testing.T) {
|
||||
got, _, err := configstore.Download(tc.version, env)
|
||||
if err != nil {
|
||||
t.Fatal("failed to download:", err)
|
||||
}
|
||||
|
||||
want := tc.want
|
||||
if !reflect.DeepEqual(*got, want) {
|
||||
t.Errorf("Download(latest, _) = %v\nwant %v", stringify(got), stringify(want))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("invalidversion", func(t *testing.T) {
|
||||
got, ver, err := configstore.Download("nonexisting", env)
|
||||
if err == nil {
|
||||
t.Fatalf("download succeeded unexpectedly: %v %+v", ver, got)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid version") {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func stringify(x any) string {
|
||||
ret, err := json.MarshalIndent(x, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("json.Marshal failed - %v", err)
|
||||
}
|
||||
return string(ret)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
|
||||
package configstore
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func init() {
|
||||
needNoConsole = needNoConsoleWindows
|
||||
}
|
||||
|
||||
func needNoConsoleWindows(cmd *exec.Cmd) {
|
||||
// The uploader main process is likely a daemonized process with no console.
|
||||
// (see x/telemetry/start_windows.go) The console creation behavior when
|
||||
// a parent is a console process without console is not clearly documented
|
||||
// but empirically we observed the new console is created and attached to the
|
||||
// subprocess in the default setup.
|
||||
//
|
||||
// Ensure no new console is attached to the subprocess by setting CREATE_NO_WINDOW.
|
||||
// https://learn.microsoft.com/en-us/windows/console/creation-of-a-console
|
||||
// https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: windows.CREATE_NO_WINDOW,
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Copyright 2024 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package configtest provides a helper for testing using a local proxy
|
||||
// containing a fake upload config.
|
||||
package configtest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/telemetry/internal/configstore"
|
||||
"golang.org/x/telemetry/internal/proxy"
|
||||
"golang.org/x/telemetry/internal/telemetry"
|
||||
)
|
||||
|
||||
// LocalProxyEnv writes a proxy directory for the given upload config, and
|
||||
// returns a go environment to use for fetching that config from a local
|
||||
// file-based proxy.
|
||||
//
|
||||
// This environment should be passed to [configstore.Download].
|
||||
func LocalProxyEnv(t *testing.T, cfg *telemetry.UploadConfig, version string) []string {
|
||||
t.Helper()
|
||||
|
||||
dir := t.TempDir()
|
||||
|
||||
encoded, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshaling config failed: %v", err)
|
||||
}
|
||||
dirPath := fmt.Sprintf("%v@%v/", configstore.ModulePath, version)
|
||||
files := map[string][]byte{
|
||||
dirPath + "go.mod": []byte("module " + configstore.ModulePath + "\n\ngo 1.20\n"),
|
||||
dirPath + "config.json": encoded,
|
||||
}
|
||||
proxyURI, err := proxy.WriteProxy(filepath.Join(dir, "proxy"), files)
|
||||
if err != nil {
|
||||
t.Fatalf("writing proxy failed: %v", err)
|
||||
}
|
||||
|
||||
env := []string{
|
||||
"GOPROXY=" + proxyURI, // Use the fake proxy.
|
||||
"GONOSUMDB=*", // Skip verifying checksum against sum.golang.org.
|
||||
"GOMODCACHE=" + filepath.Join(dir, "modcache"), // Don't pollute system module cache.
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
cmd := exec.Command("go", "clean", "-modcache")
|
||||
cmd.Env = append(cmd.Environ(), env...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Errorf("go clean -modcache failed: %v\n%s", err, out)
|
||||
}
|
||||
})
|
||||
return env
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# Telemetry Content
|
||||
|
||||
This directory contains the templates, styles, scripts, and images used in the
|
||||
telemetry services. Scripts and styles are transformed and minified by the
|
||||
generator in [content.go](./content.go).
|
||||
|
||||
## Scripts & Styles
|
||||
|
||||
The generator command will look for entrypoint scripts and styles, i.e. files
|
||||
that are not prefixed with an underscore, and minfiy their contents. TypeScript
|
||||
files are also transformed into JavaScript. See
|
||||
[devtools/cmd/esbuild](../devtools/cmd/esbuild/main.go) for more information.
|
||||
|
||||
## Templates
|
||||
|
||||
Use the .html extension to create a new route, or put an index.html file in a
|
||||
directory with the desired path. Partial templates with the extension .tmpl in
|
||||
the same directory as the requested page are included in the html/template
|
||||
execution step to allow for sharing and composing multiple templates. See
|
||||
[internal/content](../internal/content/content.go) for more information.
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package content
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
//go:embed *
|
||||
var FS embed.FS
|
||||
|
||||
//go:generate go run generate.go
|
||||
|
||||
// RunESBuild runs esbuild for all content directories.
|
||||
// If watch is set, RunESBuild instructs esbuild to watch the content
|
||||
// directories, and runs esbuild in a separate goroutine.
|
||||
func RunESBuild(watch bool) {
|
||||
_, file, _, _ := runtime.Caller(0)
|
||||
curDir := filepath.Dir(file)
|
||||
cmdDir := filepath.Join(curDir, "..", "..", "godev", "devtools", "cmd", "esbuild")
|
||||
for _, dir := range []string{"gotelemetryview", "shared", "telemetrygodev"} {
|
||||
d := filepath.Join(curDir, dir)
|
||||
args := []string{"run", ".", "--outdir", filepath.Join(d, "static")}
|
||||
if watch {
|
||||
args = append(args, "--watch", d)
|
||||
}
|
||||
args = append(args, d)
|
||||
cmd := exec.Command("go", args...)
|
||||
cmd.Dir = cmdDir
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if watch {
|
||||
go func() {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import "golang.org/x/telemetry/internal/content"
|
||||
|
||||
func main() {
|
||||
content.RunESBuild(false)
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{{define "info-icon"}}
|
||||
<details class="go-Tooltip js-tooltip" data-gtmc="tooltip">
|
||||
<summary>
|
||||
<img
|
||||
class="go-Icon"
|
||||
height="20"
|
||||
width="20"
|
||||
src="/static/info_black_24dp.svg"
|
||||
alt=""
|
||||
/>
|
||||
</summary>
|
||||
<p>
|
||||
{{.}}
|
||||
</p>
|
||||
</details>
|
||||
{{end}}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/*!
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
@import url("../shared/base.css");
|
||||
|
||||
html {
|
||||
scroll-padding-top: 4rem;
|
||||
}
|
||||
|
||||
/* TODO(rfindley): refactor to share breadcrumb logic with telemetry.go.dev */
|
||||
.ViewBreadcrumb {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ViewBreadcrumb ol {
|
||||
align-items: center;
|
||||
border-bottom: var(--border);
|
||||
display: inline-flex;
|
||||
gap: 1rem;
|
||||
list-style: none;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
padding-inline-start: 0;
|
||||
min-height: 3rem;
|
||||
width: calc(100% - 2rem);
|
||||
background-color: var(--color-background);
|
||||
padding: 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
transition: top 0.1s ease-in 0.1s;
|
||||
}
|
||||
|
||||
.ViewBreadcrumb ol:empty {
|
||||
top: -3.0625rem;
|
||||
}
|
||||
|
||||
.ViewBreadcrumb li:not(:last-child)::after {
|
||||
content: ">";
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.ViewBreadcrumb li:last-child a {
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
.Index {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.Counters {
|
||||
border: var(--border);
|
||||
border-radius: 0.25rem;
|
||||
display: grid;
|
||||
gap: 1rem 2rem;
|
||||
margin-top: 1rem;
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
grid-template-areas:
|
||||
"meta count count"
|
||||
"stack stack stack"
|
||||
"summary summary summary";
|
||||
grid-auto-columns: 1fr 2fr 1fr;
|
||||
}
|
||||
|
||||
.Meta {
|
||||
grid-area: meta;
|
||||
display: grid;
|
||||
grid-auto-rows: min-content;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.Stack {
|
||||
grid-area: stack;
|
||||
border-top: var(--border);
|
||||
padding-top: 1rem;
|
||||
gap: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Stack summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.Stack details .Count-entry:first-child::before {
|
||||
content: "⏵";
|
||||
}
|
||||
|
||||
.Stack details[open] .Count-entry:first-child::before {
|
||||
content: "⏷";
|
||||
}
|
||||
|
||||
.Count {
|
||||
grid-area: count;
|
||||
display: grid;
|
||||
flex-grow: 1;
|
||||
grid-auto-rows: min-content;
|
||||
grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
|
||||
gap: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.Summary {
|
||||
border-top: var(--border);
|
||||
font-size: 0.875rem;
|
||||
grid-area: summary;
|
||||
line-height: 1.5;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.Meta .unknown,
|
||||
.Count .unknown,
|
||||
.Stack .unknown {
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
.Count-entry {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.Count-entry > span:nth-child(odd) {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.Count-entry:not(.unknown) > span:nth-child(even) {
|
||||
text-align: right;
|
||||
color: var(--color-code-comment);
|
||||
}
|
||||
|
||||
.Count-entry > span:nth-child(odd)::after {
|
||||
content: " ----------------------------------------------------------------------------------------------- ";
|
||||
letter-spacing: 0.125rem;
|
||||
}
|
||||
|
||||
h2::after {
|
||||
content: "⏷";
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
html[data-closed-sections*="index"] h2#index::after,
|
||||
html[data-closed-sections*="config"] h2#config::after,
|
||||
html[data-closed-sections*="files"] h2#files::after,
|
||||
html[data-closed-sections*="charts"] h2#charts::after,
|
||||
html[data-closed-sections*="reports"] h2#reports::after {
|
||||
content: "⏵";
|
||||
}
|
||||
|
||||
html[data-closed-sections*="index"] h2#index ~ *,
|
||||
html[data-closed-sections*="config"] h2#config ~ *,
|
||||
html[data-closed-sections*="files"] h2#files ~ *,
|
||||
html[data-closed-sections*="charts"] h2#charts ~ *,
|
||||
html[data-closed-sections*="reports"] h2#reports ~ * {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[data-chart-id] {
|
||||
min-height: 16rem;
|
||||
}
|
||||
|
||||
/* Fix tooltip background for dark theme */
|
||||
svg g[aria-label="tip"] g {
|
||||
fill: var(--color-background);
|
||||
}
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
<!--
|
||||
Copyright 2023 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Go Telemetry</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="/static/index.min.css">
|
||||
<script src="/static/storage.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main id="main">
|
||||
<!-- TODO(rfindley): refactor to share breadcrumbs with telemetry.go.dev -->
|
||||
<nav class="ViewBreadcrumb js-breadcrumb">
|
||||
<ol></ol>
|
||||
</nav>
|
||||
|
||||
<div class="Container">
|
||||
<div class="Content">
|
||||
<h1 class="Title">Go Telemetry</h1>
|
||||
<p>
|
||||
This page allows you to inspect counters collected by Go Toolchain
|
||||
programs on your machine. It includes counters for submitted and
|
||||
pending reports. For more information about Go Toolchain telemetry
|
||||
<a target="_blank" rel="noreferrer" href="https://telemetry.go.dev/privacy">
|
||||
read the docs here.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<section class="Index">
|
||||
<h2 id="index">Index</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="#charts">Charts</a>
|
||||
<ul>
|
||||
{{range .Charts.Programs}}
|
||||
<li>
|
||||
<a href="#{{.ID}}">{{.Name}}</a>
|
||||
<ul>
|
||||
{{range .Counters}}
|
||||
<li>
|
||||
<a href="#{{.ID}}">{{.Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#config">Config</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#files">Counters</a>
|
||||
<ul>
|
||||
{{range .Files}}
|
||||
<li>
|
||||
<a href="#{{.ID}}"> {{.ID}} </a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#reports">Reports</a>
|
||||
<ul style="column-count: auto; column-width: 10rem">
|
||||
{{range .Reports}}
|
||||
<li>
|
||||
<a href="#{{.ID}}">{{.Week}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="Charts">
|
||||
<h2 id="charts">Charts</h2>
|
||||
<p>
|
||||
Charts are visualizations of the counters from your archived
|
||||
reports. Counters for different program builds of the same program
|
||||
are summed together. Use the index to navigate to charts by
|
||||
counter name.
|
||||
</p>
|
||||
{{range .Charts.Programs}}
|
||||
<div class="Chart">
|
||||
{{$pname := .Name}}
|
||||
<h3 id="{{.ID}}" data-label="{{$pname}}">
|
||||
{{$pname}}
|
||||
{{if not .Active}}
|
||||
{{template "info-icon" "This program is not present in the telemetry config."}}
|
||||
{{end}}
|
||||
</h3>
|
||||
{{range .Counters}}
|
||||
<div>
|
||||
{{$cname := .Name}}
|
||||
<h4 id="{{.ID}}" data-label="{{$cname}}">
|
||||
{{$cname}}
|
||||
{{if not .Active}}
|
||||
{{template "info-icon" "This counter is not present in the telemetry config."}}
|
||||
{{end}}
|
||||
</h4>
|
||||
<div data-chart-id="{{.ID}}"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section class="Config">
|
||||
<h2 id="config">Config</h2>
|
||||
<p>
|
||||
The config contains the list of active counters for each program
|
||||
and allowed report metadata.
|
||||
</p>
|
||||
<label>
|
||||
Version
|
||||
<select class="js-selectConfig" name="config">
|
||||
{{range .ConfigVersions}}
|
||||
<option value="{{.}}" {{if eq . $.RequestedConfig}}selected{{end}}>
|
||||
{{.}}
|
||||
</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</label>
|
||||
<pre style="max-height: 20rem">{{.PrettyConfig}}</pre>
|
||||
</section>
|
||||
|
||||
<section class="Files">
|
||||
<h2 id="files">Counters</h2>
|
||||
<p>
|
||||
Counters display data from active counter files that has not yet
|
||||
been uploaded with a report or archived. If a report includes data
|
||||
that is not registered in the telemetry config, a summary of those
|
||||
fields and how they'll be handled appears next to the counter
|
||||
values.
|
||||
</p>
|
||||
{{range .Files}}
|
||||
<div class="File">
|
||||
<h3 id="{{.ID}}">{{.ID}}</h3>
|
||||
<div class="Counters">
|
||||
<div class="Meta">
|
||||
<span>Program:</span>
|
||||
<span class="{{if not .ActiveMeta.Program}}unknown{{end}}">
|
||||
{{.Meta.Program}}
|
||||
</span>
|
||||
<span>Version:</span>
|
||||
<span class="{{if not .ActiveMeta.Version}}unknown{{end}}">
|
||||
{{.Meta.Version}}
|
||||
</span>
|
||||
<span>GOOS:</span>
|
||||
<span class="{{if not .ActiveMeta.GOOS}}unknown{{end}}">
|
||||
{{.Meta.GOOS}}
|
||||
</span>
|
||||
<span>GOARCH:</span>
|
||||
<span class="{{if not .ActiveMeta.GOARCH}}unknown{{end}}">
|
||||
{{.Meta.GOARCH}}
|
||||
</span>
|
||||
<span>GoVersion:</span>
|
||||
<span class="{{if not .ActiveMeta.GoVersion}}unknown{{end}}">
|
||||
{{.Meta.GoVersion}}
|
||||
</span>
|
||||
<span>TimeBegin:</span>
|
||||
<span>{{.Meta.TimeBegin}}</span>
|
||||
<span>TimeEnd:</span>
|
||||
<span>{{.Meta.TimeEnd}}</span>
|
||||
</div>
|
||||
{{$file := .}}
|
||||
{{with .Counts}}
|
||||
<div class="Count">
|
||||
{{range .}}
|
||||
<div class="Count-entry {{if not .Active }}unknown{{end}}">
|
||||
<span>{{.Name}}</span><span>{{.Value}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{with .Stacks}}
|
||||
<div class="Stack">
|
||||
Call stacks:
|
||||
{{range .}}
|
||||
<details>
|
||||
<summary>
|
||||
<div class="Count-entry {{if not .Active }}unknown{{end}}">
|
||||
<span>{{.Name}}</span><span>{{.Value}}</span>
|
||||
</div>
|
||||
</summary>
|
||||
<pre>{{.Trace}}</pre>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{with .Summary}}
|
||||
<div class="Summary">{{.}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
<section class="Reports">
|
||||
<h2 id="reports">Reports</h2>
|
||||
<p>
|
||||
Reports represent local copies of the data uploaded by the Go
|
||||
command to telemetry.go.dev. Use the index to navigate to a
|
||||
report by upload date or program build.
|
||||
</p>
|
||||
{{range .Reports}}
|
||||
<div class="Report">
|
||||
{{$date := .Week}}
|
||||
<h3 id="reports:{{$date}}">{{$date}}</h3>
|
||||
{{range .Programs}}
|
||||
<div id="{{.ID}}" class="Counters">
|
||||
<div class="Meta">
|
||||
<span>Program:</span>
|
||||
<span class="{{if not ($.Config.HasProgram .Program)}}unknown{{end}}">
|
||||
{{.Program}}
|
||||
</span>
|
||||
<span>Version:</span>
|
||||
<span class="{{if not ($.Config.HasVersion .Program .Version)}}unknown{{end}}">
|
||||
{{.Version}}
|
||||
</span>
|
||||
<span>GOOS:</span>
|
||||
<span class="{{if not ($.Config.HasGOOS .GOOS)}}unknown{{end}}">
|
||||
{{.GOOS}}
|
||||
</span>
|
||||
<span>GOARCH:</span>
|
||||
<span class="{{if not ($.Config.HasGOARCH .GOARCH)}}unknown{{end}}">
|
||||
{{.GOARCH}}
|
||||
</span>
|
||||
<span>GoVersion:</span>
|
||||
<span class="{{if not ($.Config.HasGoVersion .GoVersion)}}unknown{{end}}">
|
||||
{{.GoVersion}}
|
||||
</span>
|
||||
</div>
|
||||
{{$report := .}}
|
||||
{{with .Counters}}
|
||||
<div class="Count">
|
||||
{{range $name, $value := .}}
|
||||
<div class="Count-entry {{if not ($.Config.HasCounter $report.Program $name) }}unknown{{end}}">
|
||||
<span>{{$name}}</span><span>{{$value}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{with .Summary}}
|
||||
<div class="Summary">{{.}}</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
window.Page = {{.}};
|
||||
</script>
|
||||
<script src="/static/index.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
import * as Plot from "@observablehq/plot";
|
||||
|
||||
import { debounce } from "../shared/treenav";
|
||||
|
||||
declare global {
|
||||
interface Page {
|
||||
Charts: ChartData;
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
Programs: Program[];
|
||||
DateRange: [string, string];
|
||||
UploadDay: Plot.TimeIntervalName;
|
||||
}
|
||||
|
||||
interface Program {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Counters: Counter[];
|
||||
Active: boolean;
|
||||
}
|
||||
|
||||
interface Counter {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Data: Datum[];
|
||||
}
|
||||
|
||||
interface Datum {
|
||||
[key: string]: any;
|
||||
Week: string;
|
||||
Program: string;
|
||||
Version: string;
|
||||
GOARCH: string;
|
||||
GOOS: string;
|
||||
GoVersion: string;
|
||||
Key: string;
|
||||
Value: number;
|
||||
}
|
||||
const Page: Page;
|
||||
}
|
||||
|
||||
window.onload = function () {
|
||||
drawCharts();
|
||||
configSelector();
|
||||
breadcrumbController();
|
||||
sectionController();
|
||||
};
|
||||
|
||||
// sectionController adds event listeners to the section headers
|
||||
// to toggle them open and closed.
|
||||
function sectionController() {
|
||||
const html = document.querySelector("html")!;
|
||||
for (const e of document.querySelectorAll("h2")) {
|
||||
e.addEventListener("click", function () {
|
||||
let closed = localStorage.getItem("closed-sections")?.split(",");
|
||||
if (closed?.includes(this.id)) {
|
||||
closed = closed.filter((v) => v !== this.id);
|
||||
const str = closed.join(",");
|
||||
localStorage.setItem("closed-sections", str);
|
||||
html.setAttribute("data-closed-sections", str);
|
||||
} else {
|
||||
closed = [this.id].concat(closed ?? []);
|
||||
const str = closed.join(",");
|
||||
localStorage.setItem("closed-sections", str);
|
||||
html.setAttribute("data-closed-sections", str);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// drawCharts draws the charts using @observable/plot. It is called when
|
||||
// the page is first rendered and when a facet is selected.
|
||||
function drawCharts() {
|
||||
for (const program of Page.Charts.Programs ?? []) {
|
||||
for (const counter of program.Counters ?? []) {
|
||||
const rectYOpts: Plot.BinXInputs<Plot.RectYOptions> = {
|
||||
tip: true,
|
||||
x: (d: Datum) => new Date(d.Week),
|
||||
y: (d: Datum) => d.Value,
|
||||
interval: Page.Charts.UploadDay,
|
||||
fill: (d: Datum) => {
|
||||
const n = Number(d.Key);
|
||||
return isNaN(n) ? d.Key : n;
|
||||
},
|
||||
};
|
||||
|
||||
const chart = Plot.plot({
|
||||
nice: true,
|
||||
x: {
|
||||
type: "utc",
|
||||
domain: Page.Charts.DateRange.map((d) => new Date(d)),
|
||||
label: "Week",
|
||||
},
|
||||
y: {
|
||||
label: "Value",
|
||||
},
|
||||
color: {
|
||||
type: "ordinal",
|
||||
legend: true,
|
||||
scheme: "Spectral",
|
||||
reverse: true,
|
||||
label: "Counter",
|
||||
},
|
||||
height: 256,
|
||||
style: "overflow:visible;width:100%;background:transparent",
|
||||
marks: [
|
||||
Plot.rectY(counter.Data, Plot.binX({ y: "sum" }, rectYOpts)),
|
||||
Plot.ruleY([0]),
|
||||
],
|
||||
});
|
||||
document
|
||||
.querySelector(`[data-chart-id="${counter.ID}"]`)
|
||||
?.replaceChildren(chart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// configSelector adds an event listener that reloads the page when a config
|
||||
// version is selected.
|
||||
function configSelector() {
|
||||
const el = document.querySelector<HTMLButtonElement>(".js-selectConfig");
|
||||
el?.addEventListener("change", () => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
params.set(el.name, el.value);
|
||||
history.replaceState(null, "", "?" + params.toString());
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
|
||||
// breadcrumbController updates the navigation header as the user scrolls
|
||||
// that page displaying information about the content currently in the
|
||||
// viewport.
|
||||
function breadcrumbController() {
|
||||
const headings =
|
||||
document.querySelectorAll<HTMLHeadingElement>("h1, h2, h3, h4");
|
||||
const callback = debounce(() => {
|
||||
let above: HTMLHeadingElement[] = [];
|
||||
for (const h of headings) {
|
||||
const rect = h.getBoundingClientRect();
|
||||
if (rect.height && rect.top < 80) {
|
||||
above.unshift(h);
|
||||
}
|
||||
}
|
||||
if (above.length < 2) {
|
||||
above = [];
|
||||
}
|
||||
let threshold = Infinity;
|
||||
const els: HTMLHeadingElement[] = [];
|
||||
for (const h of above) {
|
||||
const level = Number(h.tagName[1]);
|
||||
if (level < threshold) {
|
||||
threshold = level;
|
||||
els.unshift(h);
|
||||
}
|
||||
}
|
||||
const breadcrumb = document.querySelector(".js-breadcrumb ol");
|
||||
const items = [];
|
||||
for (const h of els) {
|
||||
breadcrumb?.replaceChildren;
|
||||
const li = document.createElement("li");
|
||||
const a = document.createElement("a");
|
||||
a.href = `#${h.id}`;
|
||||
a.innerText = h.getAttribute("data-label") ?? h.innerText;
|
||||
li.appendChild(a);
|
||||
items.push(li);
|
||||
}
|
||||
breadcrumb?.replaceChildren(...items);
|
||||
}, 100);
|
||||
|
||||
const observer = new IntersectionObserver(callback);
|
||||
for (const h of headings) {
|
||||
observer.observe(h);
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
+14
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+74
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
+9
@@ -0,0 +1,9 @@
|
||||
// Code generated by esbuild. DO NOT EDIT.
|
||||
"use strict";(()=>{(function(){let t=localStorage.getItem("closed-sections")??"";document.querySelector("html")?.setAttribute("data-closed-sections",t)})();})();
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
//# sourceMappingURL=storage.min.js.map
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../storage.ts"],
|
||||
"sourcesContent": ["/**\n * @license\n * Copyright 2023 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n(function () {\n const closedSections = localStorage.getItem(\"closed-sections\") ?? \"\";\n const html = document.querySelector(\"html\");\n html?.setAttribute(\"data-closed-sections\", closedSections);\n})();\n"],
|
||||
"mappings": ";oBAOC,UAAY,CACX,IAAMA,EAAiB,aAAa,QAAQ,iBAAiB,GAAK,GACrD,SAAS,cAAc,MAAM,GACpC,aAAa,uBAAwBA,CAAc,CAC3D,GAAG",
|
||||
"names": ["closedSections"]
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
const closedSections = localStorage.getItem("closed-sections") ?? "";
|
||||
const html = document.querySelector("html");
|
||||
html?.setAttribute("data-closed-sections", closedSections);
|
||||
})();
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*!
|
||||
* Copyright 2021 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Colors */
|
||||
--gray-1: #202224;
|
||||
--gray-2: #3e4042;
|
||||
--gray-3: #555759;
|
||||
--gray-4: #6e7072;
|
||||
--gray-5: #848688;
|
||||
--gray-6: #aaacae;
|
||||
--gray-7: #c6c8ca;
|
||||
--gray-8: #dcdee0;
|
||||
--gray-9: #f0f1f2;
|
||||
--gray-10: #f8f8f8;
|
||||
--turq-light: #5dc9e2;
|
||||
--turq-med: #50b7e0;
|
||||
--turq-dark: #007d9c;
|
||||
--blue: #bfeaf4;
|
||||
--blue-light: #f2fafd;
|
||||
--black: #000;
|
||||
--green: #3a6e11;
|
||||
--green-light: #5fda64;
|
||||
--pink: #c85e7a;
|
||||
--pink-light: #fdecf1;
|
||||
--purple: #542c7d;
|
||||
--slate: #253443; /* Footer background. */
|
||||
--white: #fff;
|
||||
--yellow: #fceea5;
|
||||
--yellow-light: #fff8cc;
|
||||
|
||||
/* Color Intents */
|
||||
--color-brand-primary: var(--turq-dark);
|
||||
--color-background: var(--white);
|
||||
--color-background-inverted: var(--slate);
|
||||
--color-background-accented: var(--gray-10);
|
||||
--color-background-highlighted: var(--blue);
|
||||
--color-background-highlighted-link: var(--blue-light);
|
||||
--color-background-info: var(--gray-9);
|
||||
--color-background-warning: var(--yellow-light);
|
||||
--color-background-alert: var(--pink-light);
|
||||
--color-border: var(--gray-7);
|
||||
--color-text: var(--gray-1);
|
||||
--color-text-subtle: var(--gray-4);
|
||||
--color-text-link: var(--turq-dark);
|
||||
--color-text-inverted: var(--white);
|
||||
--color-code-comment: var(--green);
|
||||
|
||||
/* Interactive Colors */
|
||||
--color-input: var(--color-background);
|
||||
--color-input-text: var(--color-text);
|
||||
--color-button: var(--turq-dark);
|
||||
--color-button-disabled: var(--gray-9);
|
||||
--color-button-text: var(--white);
|
||||
--color-button-text-disabled: var(--gray-3);
|
||||
--color-button-inverted: var(--color-background);
|
||||
--color-button-inverted-disabled: var(--color-background);
|
||||
--color-button-inverted-text: var(--color-brand-primary);
|
||||
--color-button-inverted-text-disabled: var(--color-text-subtle);
|
||||
--color-button-accented: var(--yellow);
|
||||
--color-button-accented-disabled: var(--gray-9);
|
||||
--color-button-accented-text: var(--gray-1);
|
||||
--color-button-accented-text-disabled: var(--gray-3);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--color-brand-primary: var(--turq-med);
|
||||
--color-background: var(--gray-1);
|
||||
--color-background-accented: var(--gray-2);
|
||||
--color-background-highlighted: var(--gray-2);
|
||||
--color-background-highlighted-link: var(--gray-2);
|
||||
--color-background-info: var(--gray-3);
|
||||
--color-background-warning: var(--yellow);
|
||||
--color-background-alert: var(--pink);
|
||||
--color-border: var(--gray-4);
|
||||
--color-text: var(--gray-9);
|
||||
--color-text-link: var(--turq-med);
|
||||
--color-text-subtle: var(--gray-7);
|
||||
--color-code-comment: var(--green-light);
|
||||
}
|
||||
|
||||
img.go-Icon {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
+352
@@ -0,0 +1,352 @@
|
||||
/* stylelint-disable */
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input {
|
||||
/* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
/* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*!
|
||||
* Copyright 2021 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
.go-Tooltip {
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.go-Tooltip > summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.go-Tooltip > summary::-webkit-details-marker,
|
||||
.go-Tooltip > summary::marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.go-Tooltip > summary > img {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.go-Tooltip p {
|
||||
background: var(--color-background) 80%;
|
||||
border: var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.0187rem;
|
||||
line-height: 1rem;
|
||||
padding: 0.5rem;
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
white-space: normal;
|
||||
width: 12rem;
|
||||
z-index: 100;
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* ToolTipController handles closing tooltips on external clicks.
|
||||
*/
|
||||
export class ToolTipController {
|
||||
constructor(private el: HTMLDetailsElement) {
|
||||
document.addEventListener("click", (e) => {
|
||||
const insideTooltip = this.el.contains(e.target as Element);
|
||||
if (!insideTooltip) {
|
||||
this.el.removeAttribute("open");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*!
|
||||
* Copyright 2021 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
font-size: 1rem;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.4375;
|
||||
max-width: 75ch;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-bottom: var(--border);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
code,
|
||||
pre,
|
||||
textarea.code {
|
||||
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
pre,
|
||||
textarea.code {
|
||||
background-color: var(--color-background-accented);
|
||||
border: var(--border);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
overflow-x: auto;
|
||||
padding: 0.625rem;
|
||||
tab-size: 4;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
a,
|
||||
a:link,
|
||||
a:visited {
|
||||
color: var(--color-brand-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-brand-primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover > * {
|
||||
text-decoration: underline;
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*!
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
@import url("./_normalize.css");
|
||||
@import url("./_color.css");
|
||||
@import url("./_typography.css");
|
||||
@import url("./_tooltip.css");
|
||||
|
||||
:root {
|
||||
--border: 0.0625rem solid var(--color-border);
|
||||
--border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.Breadcrumb {
|
||||
background-color: var(--color-background-accented);
|
||||
}
|
||||
.Breadcrumb ol {
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
margin: 1.5rem 0;
|
||||
display: inline-flex;
|
||||
}
|
||||
.Breadcrumb li {
|
||||
display: flex;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.Breadcrumb li:not(:last-child):after {
|
||||
background: url("./arrow-forward.svg") no-repeat;
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1rem;
|
||||
margin: 0 0.8125rem;
|
||||
width: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.Hero {
|
||||
background-color: var(--color-background-accented);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
.Hero h1 {
|
||||
font-size: 2.25rem;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Container {
|
||||
margin: 0 0 5rem;
|
||||
}
|
||||
|
||||
.Content {
|
||||
margin: 0 auto;
|
||||
max-width: 64rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Copyright 2023 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{block "title" .}}{{.Title}}{{end}}</title>
|
||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
<link rel="stylesheet" href="/static/base.min.css">
|
||||
</head>
|
||||
<body>
|
||||
{{with .Breadcrumbs}}
|
||||
<nav class="Breadcrumb">
|
||||
<div class="Content">
|
||||
<ol>
|
||||
{{range .}}
|
||||
<li>{{if .Link}}<a href="{{.Link}}">{{.Label}}</a>{{else}}{{.Label}}{{end}}</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
</div>
|
||||
</nav>
|
||||
{{end}}
|
||||
<div class="Container">
|
||||
{{block "content" .}}{{.Content}}{{end}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
import { ToolTipController } from "./_tooltip";
|
||||
|
||||
for (const el of document.querySelectorAll<HTMLDetailsElement>(".js-tooltip")) {
|
||||
new ToolTipController(el);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*!
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
@import url("../shared/treenav.css");
|
||||
|
||||
/* Fix tooltip background for dark theme */
|
||||
svg g[aria-label="tip"] g {
|
||||
fill: var(--color-background);
|
||||
}
|
||||
|
||||
.Chartbrowser-view {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.Chartbrowser-index {
|
||||
flex: 1 1;
|
||||
padding: 0 1.5rem 0 0;
|
||||
}
|
||||
.Chartbrowser-heading {
|
||||
font-weight: bold;
|
||||
font-size: 1.25rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
.Chartbrowser-index-sticky {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
width: 10rem;
|
||||
}
|
||||
.Chartbrowser-index-sticky > ul {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
.Chartbrowser-link {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: .875rem;
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
.Chartbrowser-program {
|
||||
font-weight: normal;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
.Chartbrowser-program:not(:first-of-type) {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.Chartbrowser-chart {
|
||||
background-color: var(--color-background);
|
||||
border: 1px solid transparent;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.875rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);
|
||||
}
|
||||
.Chartbrowser-chart-name {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
Copyright 2024 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
<!--
|
||||
A chart browser is a reusable component for displaying a collection of
|
||||
charts.
|
||||
-->
|
||||
|
||||
{{define "chartbrowser"}}
|
||||
<div class="Chartbrowser-view js-Tree">
|
||||
<div class="Chartbrowser-index">
|
||||
<nav class="Chartbrowser-index-sticky">
|
||||
<h3 class="Chartbrowser-heading">Charts</h2>
|
||||
<ul>
|
||||
{{range .Charts.Programs}}
|
||||
{{if .Charts}}
|
||||
<li class="js-Tree-item" data-heading-id="{{.ID}}">
|
||||
<a class="Chartbrowser-link" href="#{{.ID}}">{{programName .Name}}</a>
|
||||
<ul>
|
||||
{{range .Charts}}
|
||||
{{with .}}
|
||||
<li class="js-Tree-item" data-heading-id="{{.ID}}">
|
||||
<a class="Chartbrowser-link" href="#{{.ID}}">{{chartName .Name}}</a>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
<div class="Chartbrowser-content">
|
||||
{{range .Charts.Programs}}
|
||||
{{if .Charts}}
|
||||
{{$progName := programName .Name}}
|
||||
<h3 id="{{.ID}}" class="Chartbrowser-program js-Tree-heading">{{$progName}}</h3>
|
||||
{{range .Charts}}
|
||||
{{with .}}
|
||||
<div class="Chartbrowser-chart">
|
||||
<h4 id="{{.ID}}" class="Chartbrowser-chart-name js-Tree-heading">{{$progName}} > {{chartName .Name}}</h4>
|
||||
<div class="Chart-chart" data-chart-id="{{.ID}}"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<!--
|
||||
Copyright 2023 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{block "title" .}}{{.Title}}{{end}}</title>
|
||||
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
|
||||
<link rel="stylesheet" href="/static/base.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="Container">
|
||||
<div class="Content">
|
||||
{{block "content" .}}{{.Content}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="5" y="2" width="7" height="12">
|
||||
<path d="M5.06 12.3934L6 13.3334L11.3333 8.00002L6 2.66669L5.06 3.60669L9.44666 8.00002" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0)">
|
||||
<rect width="16" height="16" fill="#5F6368"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 386 B |
+14
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+15
@@ -0,0 +1,15 @@
|
||||
// Code generated by esbuild. DO NOT EDIT.
|
||||
"use strict";(()=>{var e=class{constructor(i){this.el=i;document.addEventListener("click",o=>{this.el.contains(o.target)||this.el.removeAttribute("open")})}};for(let t of document.querySelectorAll(".js-tooltip"))new e(t);})();
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2021 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
//# sourceMappingURL=base.min.js.map
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../_tooltip.ts", "../base.ts"],
|
||||
"sourcesContent": ["/**\n * @license\n * Copyright 2021 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * ToolTipController handles closing tooltips on external clicks.\n */\nexport class ToolTipController {\n constructor(private el: HTMLDetailsElement) {\n document.addEventListener(\"click\", (e) => {\n const insideTooltip = this.el.contains(e.target as Element);\n if (!insideTooltip) {\n this.el.removeAttribute(\"open\");\n }\n });\n }\n}\n", "/**\n * @license\n * Copyright 2023 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\nimport { ToolTipController } from \"./_tooltip\";\n\nfor (const el of document.querySelectorAll<HTMLDetailsElement>(\".js-tooltip\")) {\n new ToolTipController(el);\n}\n"],
|
||||
"mappings": ";mBAUO,IAAMA,EAAN,KAAwB,CAC7B,YAAoBC,EAAwB,CAAxB,QAAAA,EAClB,SAAS,iBAAiB,QAAUC,GAAM,CAClB,KAAK,GAAG,SAASA,EAAE,MAAiB,GAExD,KAAK,GAAG,gBAAgB,MAAM,CAElC,CAAC,CACH,CACF,ECVA,QAAWC,KAAM,SAAS,iBAAqC,aAAa,EAC1E,IAAIC,EAAkBD,CAAE",
|
||||
"names": ["ToolTipController", "el", "e", "el", "ToolTipController"]
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/* Code generated by esbuild. DO NOT EDIT. */
|
||||
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}svg g[aria-label=tip] g{fill:var(--color-background)}.Chartbrowser-view{display:flex;flex-direction:row}.Chartbrowser-index{flex:1 1;padding:0 1.5rem 0 0}.Chartbrowser-heading{font-weight:700;font-size:1.25rem;margin:0 0 .5rem}.Chartbrowser-index-sticky{position:sticky;top:1rem;width:10rem}.Chartbrowser-index-sticky>ul{position:sticky;top:1rem;margin-top:0}.Chartbrowser-link{color:var(--color-text-subtle);font-size:.875rem;line-height:1.5rem}.Chartbrowser-program{font-weight:400;margin:0 0 1rem}.Chartbrowser-program:not(:first-of-type){margin-top:2rem}.Chartbrowser-chart{background-color:var(--color-background);border:1px solid transparent;margin-bottom:1rem;padding:.875rem;box-shadow:0 1px 2px #3c40434d,0 1px 3px 1px #3c404326}.Chartbrowser-chart-name{text-align:center;margin:0}
|
||||
/*!
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
/*# sourceMappingURL=chartbrowser.min.css.map */
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../treenav.css", "../chartbrowser.css"],
|
||||
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/treenav.css\");\n\n/* Fix tooltip background for dark theme */\nsvg g[aria-label=\"tip\"] g {\n fill: var(--color-background);\n}\n\n.Chartbrowser-view {\n display: flex;\n flex-direction: row;\n}\n.Chartbrowser-index {\n flex: 1 1;\n padding: 0 1.5rem 0 0;\n}\n.Chartbrowser-heading {\n font-weight: bold;\n font-size: 1.25rem;\n margin: 0 0 0.5rem 0;\n}\n.Chartbrowser-index-sticky {\n position: sticky;\n top: 1rem;\n width: 10rem;\n}\n.Chartbrowser-index-sticky > ul {\n position: sticky;\n top: 1rem;\n margin-top: 0;\n}\n.Chartbrowser-link {\n color: var(--color-text-subtle);\n font-size: .875rem;\n line-height: 1.5rem;\n}\n.Chartbrowser-program {\n font-weight: normal;\n margin: 0 0 1rem 0;\n}\n.Chartbrowser-program:not(:first-of-type) {\n margin-top: 2rem;\n}\n.Chartbrowser-chart {\n background-color: var(--color-background);\n border: 1px solid transparent;\n margin-bottom: 1rem;\n padding: 0.875rem;\n box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);\n}\n.Chartbrowser-chart-name {\n text-align: center;\n margin: 0;\n}\n"],
|
||||
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE,wBCvCF,wBACE,6BAGF,mBACE,aACA,mBAEF,oBACE,SAlBF,qBAqBA,sBACE,gBACA,kBAvBF,iBA0BA,2BACE,gBACA,SACA,YAEF,8BACE,gBACA,SACA,aAEF,mBACE,+BACA,kBACA,mBAEF,sBACE,gBA1CF,gBA6CA,0CACE,gBAEF,oBACE,yCACA,6BACA,mBAnDF,gBAqDE,uDAEF,yBACE,kBAxDF",
|
||||
"names": []
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
+8
@@ -0,0 +1,8 @@
|
||||
/* Code generated by esbuild. DO NOT EDIT. */
|
||||
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}
|
||||
/*!
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
/*# sourceMappingURL=treenav.min.css.map */
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../treenav.css"],
|
||||
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n"],
|
||||
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE",
|
||||
"names": []
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// Code generated by esbuild. DO NOT EDIT.
|
||||
"use strict";(()=>{function h(r){let n=r.querySelectorAll(".js-Tree-heading"),s=()=>{let o=[];for(let e of n){let t=e.getBoundingClientRect();t.height&&t.top<80&&o.unshift(e)}o.length==0&&n[0]instanceof HTMLHeadingElement&&(o=[n[0]]);let l=1/0,a=[];for(let e of o){let t=Number(e.tagName[1]);t<l&&(l=t,a.push(e))}let d=r.querySelectorAll(".js-Tree-item");for(let e of d){let t=e.dataset.headingId,c=!1,f=!1;for(let u of a)if(u.id===t){u===a[0]?c=!0:f=!0;break}e.setAttribute("aria-selected",c?"true":"false"),e.setAttribute("aria-expanded",f?"true":"false")}},i=new IntersectionObserver(m(s,20));for(let o of n)i.observe(o)}function m(r,n){let s;return(...i)=>{clearTimeout(s),s=setTimeout(()=>r(...i),n)}}})();
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
//# sourceMappingURL=treenav.min.js.map
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../treenav.ts"],
|
||||
"sourcesContent": ["/**\n * @license\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n/**\n * A treeNavController adds dynamic expansion and selection of index list\n * elements based on scroll position.\n *\n * Use it as follows:\n * - Add the .js-Tree class to a parent element of your index and content.\n * - Add the .js-Tree-item class to <li> elements of your index.\n * - Add the .js-Tree-heading class to <hN> heading elements of your content.\n *\n * Then, when you scroll content, the 'aria-selected' and 'aria-expanded'\n * attributes of your tree items will be set according to the current content\n * scroll position. The included treenav.css implements styling to expand and\n * highlight index elements according to these attributes.\n */\nexport function treeNavController(el: HTMLElement) {\n const headings = el.querySelectorAll<HTMLHeadingElement>(\".js-Tree-heading\");\n const callback = () => {\n // Collect heading elements above the scroll position.\n let above: HTMLHeadingElement[] = [];\n for (const h of headings) {\n const rect = h.getBoundingClientRect();\n if (rect.height && rect.top < 80) {\n above.unshift(h);\n }\n }\n // Highlight the first heading even if we're not yet scrolled below it.\n if (above.length == 0 && headings[0] instanceof HTMLHeadingElement) {\n above = [headings[0]];\n }\n // Collect the set of heading levels we're immediately below, at most one\n // per heading level, by decresing level.\n // e.g. [<h3 element>, <h2 element>, <h1 element>]\n let threshold = Infinity;\n const active: HTMLHeadingElement[] = [];\n for (const h of above) {\n const level = Number(h.tagName[1]);\n if (level < threshold) {\n threshold = level;\n active.push(h);\n }\n }\n // Update aria-selected and aria-expanded for all items, per the current\n // position.\n const navItems = el.querySelectorAll<HTMLElement>(\".js-Tree-item\");\n for (const item of navItems) {\n const headingId = item.dataset[\"headingId\"];\n let selected = false,\n expanded = false;\n for (const h of active) {\n if (h.id === headingId) {\n if (h === active[0]) {\n selected = true;\n } else {\n expanded = true;\n }\n break;\n }\n }\n item.setAttribute(\"aria-selected\", selected ? \"true\" : \"false\");\n item.setAttribute(\"aria-expanded\", expanded ? \"true\" : \"false\");\n }\n };\n\n // Update on changes to viewport intersection, defensively debouncing to\n // guard against performance issues.\n const observer = new IntersectionObserver(debounce(callback, 20));\n for (const h of headings) {\n observer.observe(h);\n }\n}\n\nexport function debounce<T extends (...args: unknown[]) => unknown>(\n callback: T,\n wait: number\n) {\n let timeout: number;\n return (...args: unknown[]) => {\n clearTimeout(timeout);\n timeout = setTimeout(() => callback(...args), wait);\n };\n}\n"],
|
||||
"mappings": ";mBAqBO,SAASA,EAAkBC,EAAiB,CACjD,IAAMC,EAAWD,EAAG,iBAAqC,kBAAkB,EACrEE,EAAW,IAAM,CAErB,IAAIC,EAA8B,CAAC,EACnC,QAAWC,KAAKH,EAAU,CACxB,IAAMI,EAAOD,EAAE,sBAAsB,EACjCC,EAAK,QAAUA,EAAK,IAAM,IAC5BF,EAAM,QAAQC,CAAC,EAIfD,EAAM,QAAU,GAAKF,EAAS,CAAC,YAAa,qBAC9CE,EAAQ,CAACF,EAAS,CAAC,CAAC,GAKtB,IAAIK,EAAY,IACVC,EAA+B,CAAC,EACtC,QAAWH,KAAKD,EAAO,CACrB,IAAMK,EAAQ,OAAOJ,EAAE,QAAQ,CAAC,CAAC,EAC7BI,EAAQF,IACVA,EAAYE,EACZD,EAAO,KAAKH,CAAC,GAKjB,IAAMK,EAAWT,EAAG,iBAA8B,eAAe,EACjE,QAAWU,KAAQD,EAAU,CAC3B,IAAME,EAAYD,EAAK,QAAQ,UAC3BE,EAAW,GACbC,EAAW,GACb,QAAWT,KAAKG,EACd,GAAIH,EAAE,KAAOO,EAAW,CAClBP,IAAMG,EAAO,CAAC,EAChBK,EAAW,GAEXC,EAAW,GAEb,MAGJH,EAAK,aAAa,gBAAiBE,EAAW,OAAS,OAAO,EAC9DF,EAAK,aAAa,gBAAiBG,EAAW,OAAS,OAAO,EAElE,EAIMC,EAAW,IAAI,qBAAqBC,EAASb,EAAU,EAAE,CAAC,EAChE,QAAWE,KAAKH,EACda,EAAS,QAAQV,CAAC,CAEtB,CAEO,SAASW,EACdb,EACAc,EACA,CACA,IAAIC,EACJ,MAAO,IAAIC,IAAoB,CAC7B,aAAaD,CAAO,EACpBA,EAAU,WAAW,IAAMf,EAAS,GAAGgB,CAAI,EAAGF,CAAI,CACpD,CACF",
|
||||
"names": ["treeNavController", "el", "headings", "callback", "above", "h", "rect", "threshold", "active", "level", "navItems", "item", "headingId", "selected", "expanded", "observer", "debounce", "wait", "timeout", "args"]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*!
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
.js-Tree ul {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.js-Tree-item ul {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.js-Tree-item {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0.125rem 0 0.125rem 0;
|
||||
}
|
||||
|
||||
.js-Tree-item[aria-expanded='true'] ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.js-Tree-item .js-Tree-item {
|
||||
position: relative;
|
||||
padding-left: 1.25rem;
|
||||
}
|
||||
|
||||
.js-Tree-item .js-Tree-item[aria-selected='true']:before {
|
||||
background-color: var(--color-brand-primary);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
display: block;
|
||||
height: .3125rem;
|
||||
left: .4688rem;
|
||||
position: absolute;
|
||||
top: .75rem;
|
||||
width: .3125rem;
|
||||
}
|
||||
|
||||
.js-Tree-item>a {
|
||||
color: var(--color-text-subtle);
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.js-Tree-item[aria-selected='true']>a {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A treeNavController adds dynamic expansion and selection of index list
|
||||
* elements based on scroll position.
|
||||
*
|
||||
* Use it as follows:
|
||||
* - Add the .js-Tree class to a parent element of your index and content.
|
||||
* - Add the .js-Tree-item class to <li> elements of your index.
|
||||
* - Add the .js-Tree-heading class to <hN> heading elements of your content.
|
||||
*
|
||||
* Then, when you scroll content, the 'aria-selected' and 'aria-expanded'
|
||||
* attributes of your tree items will be set according to the current content
|
||||
* scroll position. The included treenav.css implements styling to expand and
|
||||
* highlight index elements according to these attributes.
|
||||
*/
|
||||
export function treeNavController(el: HTMLElement) {
|
||||
const headings = el.querySelectorAll<HTMLHeadingElement>(".js-Tree-heading");
|
||||
const callback = () => {
|
||||
// Collect heading elements above the scroll position.
|
||||
let above: HTMLHeadingElement[] = [];
|
||||
for (const h of headings) {
|
||||
const rect = h.getBoundingClientRect();
|
||||
if (rect.height && rect.top < 80) {
|
||||
above.unshift(h);
|
||||
}
|
||||
}
|
||||
// Highlight the first heading even if we're not yet scrolled below it.
|
||||
if (above.length == 0 && headings[0] instanceof HTMLHeadingElement) {
|
||||
above = [headings[0]];
|
||||
}
|
||||
// Collect the set of heading levels we're immediately below, at most one
|
||||
// per heading level, by decresing level.
|
||||
// e.g. [<h3 element>, <h2 element>, <h1 element>]
|
||||
let threshold = Infinity;
|
||||
const active: HTMLHeadingElement[] = [];
|
||||
for (const h of above) {
|
||||
const level = Number(h.tagName[1]);
|
||||
if (level < threshold) {
|
||||
threshold = level;
|
||||
active.push(h);
|
||||
}
|
||||
}
|
||||
// Update aria-selected and aria-expanded for all items, per the current
|
||||
// position.
|
||||
const navItems = el.querySelectorAll<HTMLElement>(".js-Tree-item");
|
||||
for (const item of navItems) {
|
||||
const headingId = item.dataset["headingId"];
|
||||
let selected = false,
|
||||
expanded = false;
|
||||
for (const h of active) {
|
||||
if (h.id === headingId) {
|
||||
if (h === active[0]) {
|
||||
selected = true;
|
||||
} else {
|
||||
expanded = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
item.setAttribute("aria-selected", selected ? "true" : "false");
|
||||
item.setAttribute("aria-expanded", expanded ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
// Update on changes to viewport intersection, defensively debouncing to
|
||||
// guard against performance issues.
|
||||
const observer = new IntersectionObserver(debounce(callback, 20));
|
||||
for (const h of headings) {
|
||||
observer.observe(h);
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce<T extends (...args: unknown[]) => unknown>(
|
||||
callback: T,
|
||||
wait: number
|
||||
) {
|
||||
let timeout: number;
|
||||
return (...args: unknown[]) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => callback(...args), wait);
|
||||
};
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<!--
|
||||
Copyright 2024 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Go Telemetry / Daily Charts{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
|
||||
<main id="main">
|
||||
<section>
|
||||
<div class="Hero">
|
||||
<div class="Content">
|
||||
<h1>Daily Charts</h1>
|
||||
<p>View charts for telemetry data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="Content">
|
||||
<ul style="column-count: auto; column-width: 10rem">
|
||||
{{range .}}
|
||||
<li><a href="/charts/{{.}}">{{.}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
{{end}}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*!
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
|
||||
@import url("../shared/chartbrowser.css");
|
||||
|
||||
.Charts {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<!--
|
||||
Copyright 2023 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Go Telemetry / {{index .Charts.DateRange 1}}{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<link rel="stylesheet" href="/static/charts.min.css">
|
||||
|
||||
<main id="main">
|
||||
<section>
|
||||
<div class="Hero">
|
||||
<div class="Content">
|
||||
<h1>{{.ChartTitle}}</h1>
|
||||
<p>Generated from {{.Charts.NumReports}} reports.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="Content">
|
||||
<div class="Charts">
|
||||
{{template "chartbrowser" .}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
window.Page = {{.}};
|
||||
</script>
|
||||
<script src="/static/charts.min.js"></script>
|
||||
|
||||
{{end}}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
interface Page {
|
||||
Charts: ChartData | null;
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
Programs: Program[] | null;
|
||||
}
|
||||
|
||||
interface Program {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Charts: Chart[] | null;
|
||||
}
|
||||
|
||||
interface Chart {
|
||||
ID: string;
|
||||
Name: string;
|
||||
Type: string;
|
||||
Data: Datum[] | null;
|
||||
}
|
||||
|
||||
interface Datum {
|
||||
Key: string;
|
||||
Value: number;
|
||||
}
|
||||
|
||||
declare const Page: Page;
|
||||
|
||||
import * as d3 from "d3";
|
||||
import * as Plot from "@observablehq/plot";
|
||||
import { treeNavController } from "../shared/treenav";
|
||||
|
||||
for (const program of Page.Charts?.Programs || []) {
|
||||
for (const counter of program?.Charts || []) {
|
||||
switch (counter.Type) {
|
||||
case "partition":
|
||||
document
|
||||
.querySelector(`[data-chart-id="${counter.ID}"]`)
|
||||
?.append(partition(counter));
|
||||
|
||||
break;
|
||||
case "histogram":
|
||||
document
|
||||
.querySelector(`[data-chart-id="${counter.ID}"]`)
|
||||
?.append(histogram(counter));
|
||||
|
||||
break;
|
||||
default:
|
||||
console.error("unknown chart type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const el of document.querySelectorAll<HTMLElement>(".js-Tree")) {
|
||||
treeNavController(el);
|
||||
}
|
||||
|
||||
function partition({ Data, Name }: Chart) {
|
||||
Data ??= [];
|
||||
|
||||
const max = Data.map((d) => d.Value).reduce((a, b) => Math.max(a, b), 0);
|
||||
|
||||
return Plot.plot({
|
||||
color: {
|
||||
type: "categorical",
|
||||
scheme: "set2",
|
||||
},
|
||||
nice: true,
|
||||
x: {
|
||||
label: Name,
|
||||
labelOffset: Number.MAX_SAFE_INTEGER,
|
||||
tickRotate: 45,
|
||||
domain: Data.map((d) => d.Key),
|
||||
},
|
||||
y: {
|
||||
label: "Reports", // currently, partition charts count the number of reports, not counter totals.
|
||||
domain: [0, max + 1], // adjust domain to prevent rendering issues, especially with all-zero data.
|
||||
},
|
||||
width: 1024,
|
||||
style: {
|
||||
overflow: "visible",
|
||||
background: "transparent",
|
||||
marginBottom: "3rem",
|
||||
fontSize: "0.8rem",
|
||||
marginTop: "1rem",
|
||||
},
|
||||
insetTop: 20, // leave enough space between the axis label and marks
|
||||
marks: [
|
||||
Plot.barY(Data, {
|
||||
tip: true,
|
||||
fill: (d) => (isNaN(Number(d.Key)) ? d.Key : Number(d.Key)),
|
||||
x: (d) => d.Key,
|
||||
y: (d) => d.Value,
|
||||
}),
|
||||
Plot.frame(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function histogram({ Data }: Chart) {
|
||||
Data ??= [];
|
||||
const n = 3; // number of facet columns
|
||||
const fixKey = (k: string) => (isNaN(Number(k)) ? k : Number(k));
|
||||
const keys = Array.from(d3.union(Data.map((d) => fixKey(d.Key))));
|
||||
const index = new Map(keys.map((key, i) => [key, i]));
|
||||
const fx = (key: string | number) => (index.get(key) ?? 0) % n;
|
||||
const fy = (key: string | number) => Math.floor((index.get(key) ?? 0) / n);
|
||||
|
||||
return Plot.plot({
|
||||
marginLeft: 60,
|
||||
width: 1024,
|
||||
grid: true,
|
||||
nice: true,
|
||||
x: {
|
||||
label: "Distribution",
|
||||
},
|
||||
color: {
|
||||
type: "ordinal",
|
||||
legend: true,
|
||||
scheme: "Spectral",
|
||||
label: "Counter",
|
||||
},
|
||||
y: {
|
||||
insetTop: 16,
|
||||
domain: [0, 1],
|
||||
},
|
||||
fx: {
|
||||
ticks: [],
|
||||
},
|
||||
fy: {
|
||||
ticks: [],
|
||||
},
|
||||
style: "background:transparent;",
|
||||
marks: [
|
||||
Plot.barY(
|
||||
Data,
|
||||
Plot.binX(
|
||||
{ y: "proportion-facet", x: "x1", interval: 0.1, cumulative: 1 },
|
||||
{
|
||||
tip: true,
|
||||
fill: (d: Datum) => fixKey(d.Key),
|
||||
x: (d: Datum) => d.Value,
|
||||
fx: (d: Datum) => fx(fixKey(d.Key)),
|
||||
fy: (d: Datum) => fy(fixKey(d.Key)),
|
||||
}
|
||||
)
|
||||
),
|
||||
Plot.text(keys, {
|
||||
frameAnchor: "top",
|
||||
dy: 3,
|
||||
fx,
|
||||
fy,
|
||||
}),
|
||||
Plot.axisX({ anchor: "bottom", tickSpacing: 35 }),
|
||||
Plot.axisX({ anchor: "top", tickSpacing: 35 }),
|
||||
Plot.frame(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export {};
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<!--
|
||||
Copyright 2023 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Go Telemetry Config{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<main id="main">
|
||||
<div class="Content">
|
||||
<section class="Chart Config">
|
||||
<h2 id="config">Chart Config</h2>
|
||||
<p>
|
||||
The chart config contains the list of approved charts to display on
|
||||
telemetry.go.dev. The chart config format is documented by the
|
||||
<a href="https://pkg.go.dev/golang.org/x/telemetry/internal/chartconfig">
|
||||
<code>chartconfig</code>
|
||||
</a> package documentation.
|
||||
</p>
|
||||
<pre style="max-height: 100rem">{{.ChartConfig}}</pre>
|
||||
</section>
|
||||
|
||||
<section class="Upload Config">
|
||||
<h2 id="config">Upload Config</h2>
|
||||
<p>
|
||||
The upload config contains the list of active counters for each program
|
||||
and allowed report metadata. This is generated from the chart config
|
||||
above.
|
||||
</p>
|
||||
<label>
|
||||
Version: {{.Version}}
|
||||
</label>
|
||||
<pre style="max-height: 100rem">{{.UploadConfig}}</pre>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{{end}}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Copyright 2024 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Go Telemetry / Data{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
|
||||
<main id="main">
|
||||
<section>
|
||||
<div class="Hero">
|
||||
<div class="Content">
|
||||
<h1>Merged daily reports</h1>
|
||||
<p>Download raw telemetry data.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="Content">
|
||||
<ul style="margin-top: 1.5rem; column-count: auto; column-width: 10rem">
|
||||
{{$url := .BucketURL}}
|
||||
{{range .Dates}}
|
||||
<li><a href="{{$url}}/{{.}}.json">{{.}}</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
{{end}}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*!
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
|
||||
@import url("../shared/chartbrowser.css");
|
||||
|
||||
p {
|
||||
/* Reset from _typography.css */
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.Charts {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.Centered {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.Charts-heading {
|
||||
margin: 2.5rem 0 1.5rem;
|
||||
}
|
||||
.Charts-heading h2 {
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
}
|
||||
.Charts-heading a {
|
||||
border: var(--border);
|
||||
padding: 0.6rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: var(--color-background-highlighted-link);
|
||||
}
|
||||
.Charts-heading ul {
|
||||
margin: 0.5rem 0 0 0;
|
||||
list-style: none;
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
}
|
||||
.Charts-heading li {
|
||||
list-style: none;
|
||||
display: inline-flex;
|
||||
}
|
||||
.Charts-heading li:not(:last-child) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
Copyright 2023 The Go Authors. All rights reserved.
|
||||
Use of this source code is governed by a BSD-style
|
||||
license that can be found in the LICENSE file.
|
||||
-->
|
||||
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Go Telemetry{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<link rel="stylesheet" href="/static/index.min.css">
|
||||
|
||||
<main id="main">
|
||||
<section>
|
||||
<div class="Hero">
|
||||
<div class="Content">
|
||||
<h1>Go Telemetry 📊</h1>
|
||||
<p>
|
||||
<em>Go Telemetry</em> is a way for Go toolchain programs to collect
|
||||
data about their performance and usage. Uploaded data is used to help
|
||||
improve the Go toolchain and related tools. Go Telemetry is not built
|
||||
into users' binaries. Learn more about Go telemetry at
|
||||
<a href="https://go.dev/doc/telemetry">go.dev/doc/telemetry</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Users who have opted in will upload an approved subset of telemetry
|
||||
data approximately once a week. This subset is determined by the current
|
||||
<a href="/config">upload configuration</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
For privacy information about this service, see
|
||||
<a href="/privacy">telemetry.go.dev/privacy</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="Charts">
|
||||
<div class="Content">
|
||||
<div class="Charts-heading">
|
||||
<h2>{{.ChartTitle}}</h2>
|
||||
<ul>
|
||||
<li><a href="/charts/">All charts</a></li>
|
||||
<li><a href="/data/">All raw data</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{template "chartbrowser" .}}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
window.Page = {{.}};
|
||||
</script>
|
||||
<script src="/static/charts.min.js"></script>
|
||||
|
||||
{{end}}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
---
|
||||
Title: Go Telemetry Privacy Policy
|
||||
Layout: privacy.html
|
||||
---
|
||||
|
||||
# Privacy Policy
|
||||
|
||||
_Last updated: January 24, 2024_
|
||||
|
||||
Go Telemetry is a way for Go toolchain programs to collect data about their
|
||||
performance and usage. This data can help developers improve the language and
|
||||
tools.
|
||||
|
||||
## What Go Telemetry Records {#collection}
|
||||
|
||||
Go toolchain programs, such as the `go` command and `gopls`, record certain information
|
||||
about their own execution. This data is stored in local files on your computer,
|
||||
specifically in the [`os.UserConfigDir()/go/telemetry/local`](https://pkg.go.dev/os#UserConfigDir) directory.
|
||||
|
||||
Here is what these files contain:
|
||||
|
||||
* Event counters: Information about how Go toolchain programs
|
||||
are used.
|
||||
* Stack traces: Details about program execution for troubleshooting.
|
||||
* Basic system information: Your operating system, CPU architecture, and name and version of the Go tool being executed.
|
||||
|
||||
Importantly, these files do not contain personal or other
|
||||
identifying information about you or your system.
|
||||
|
||||
## Data Privacy {#data-privacy}
|
||||
|
||||
By default, the data collected by Go Telemetry is kept only locally on your computer.
|
||||
|
||||
It is not shared with anyone unless you explicitly decide to enable Go Telemetry.
|
||||
You can do this by running the command [`gotelemetry on`](#command) or using a command
|
||||
in your integrated development environment (IDE).
|
||||
|
||||
Once enabled, Go Telemetry may decide once a week to upload reports to a Google
|
||||
server. A local copy of the uploaded reports is kept in the
|
||||
[`os.UserConfigDir()/go/telemetry/remote`](https://pkg.go.dev/os#UserConfigDir) directory on the user's machine.
|
||||
These reports include only approved counters and are collected in
|
||||
accordance with the Google Privacy Policy, which you can find
|
||||
at [Google Privacy Policy](https://policies.google.com/privacy).
|
||||
|
||||
The uploaded reports are also made available as part of a public dataset at
|
||||
[telemetry.go.dev](https://telemetry.go.dev). Developers working on Go,
|
||||
both inside and outside of Google, use this dataset to understand
|
||||
how the Go toolchain is used and if it is performing as expected.
|
||||
|
||||
## Using the `gotelemetry` Command Line Tool {#command}
|
||||
|
||||
To manage Go Telemetry, you can use the `gotelemetry` command line tool.
|
||||
|
||||
go install golang.org/x/telemetry/cmd/gotelemetry@latest
|
||||
|
||||
Here are some useful commands:
|
||||
|
||||
* `gotelemetry on`: Upload Go Telemetry data weekly.
|
||||
* `gotelemetry off`: Do not upload Go Telemetry data.
|
||||
* `gotelemetry view`: View locally collected telemetry data.
|
||||
* `gotelemetry clear`: Clear locally collected telemetry data at any time.
|
||||
|
||||
For the complete usage documentation of the gotelemetry command line tool, visit
|
||||
[golang.org/x/telemetry/cmd/gotelemetry](https://golang.org/x/telemetry/cmd/gotelemetry).
|
||||
|
||||
|
||||
## Approved Counters {#config}
|
||||
|
||||
Go Telemetry only uploads counters that have been approved through the [public proposal process](https://github.com/orgs/golang/projects/29).
|
||||
You can find the set of approved counters as a Go module at
|
||||
[golang.org/x/telemetry/config](https://go.googlesource.com/telemetry/+/refs/heads/master/config/config.json) and the [current config in use](https://telemetry.go.dev/config).
|
||||
|
||||
## IDE Integration {#integration}
|
||||
|
||||
If you're using an integrated development environment (IDE) like Visual Studio
|
||||
Code, versions
|
||||
[`v0.14.0`](https://github.com/golang/tools/releases/tag/gopls%2Fv0.14.0) and
|
||||
later of the Go language server [gopls](https://go.dev/s/gopls) collect
|
||||
telemetry data. As described above, data is only uploaded after you have opted
|
||||
in, either by using the command [`gotelemetry on`](#command) as described above
|
||||
or by accepting a dialog in the IDE.
|
||||
|
||||
You can always opt out of uploading at any time by using the
|
||||
[`gotelemetry local`](#command) or [`gotelemetry off`](#command) commands.
|
||||
|
||||
By sharing performance statistics, usage information, and crash reports with Go Telemetry,
|
||||
you can help improve the Go programming language and its tools while also ensuring
|
||||
your data privacy.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
/* Code generated by esbuild. DO NOT EDIT. */
|
||||
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}svg g[aria-label=tip] g{fill:var(--color-background)}.Chartbrowser-view{display:flex;flex-direction:row}.Chartbrowser-index{flex:1 1;padding:0 1.5rem 0 0}.Chartbrowser-heading{font-weight:700;font-size:1.25rem;margin:0 0 .5rem}.Chartbrowser-index-sticky{position:sticky;top:1rem;width:10rem}.Chartbrowser-index-sticky>ul{position:sticky;top:1rem;margin-top:0}.Chartbrowser-link{color:var(--color-text-subtle);font-size:.875rem;line-height:1.5rem}.Chartbrowser-program{font-weight:400;margin:0 0 1rem}.Chartbrowser-program:not(:first-of-type){margin-top:2rem}.Chartbrowser-chart{background-color:var(--color-background);border:1px solid transparent;margin-bottom:1rem;padding:.875rem;box-shadow:0 1px 2px #3c40434d,0 1px 3px 1px #3c404326}.Chartbrowser-chart-name{text-align:center;margin:0}.Charts{margin-top:1.5rem}
|
||||
/*!
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
/*!
|
||||
* Copyright 2023 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
/*# sourceMappingURL=charts.min.css.map */
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../shared/treenav.css", "../../shared/chartbrowser.css", "../charts.css"],
|
||||
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/treenav.css\");\n\n/* Fix tooltip background for dark theme */\nsvg g[aria-label=\"tip\"] g {\n fill: var(--color-background);\n}\n\n.Chartbrowser-view {\n display: flex;\n flex-direction: row;\n}\n.Chartbrowser-index {\n flex: 1 1;\n padding: 0 1.5rem 0 0;\n}\n.Chartbrowser-heading {\n font-weight: bold;\n font-size: 1.25rem;\n margin: 0 0 0.5rem 0;\n}\n.Chartbrowser-index-sticky {\n position: sticky;\n top: 1rem;\n width: 10rem;\n}\n.Chartbrowser-index-sticky > ul {\n position: sticky;\n top: 1rem;\n margin-top: 0;\n}\n.Chartbrowser-link {\n color: var(--color-text-subtle);\n font-size: .875rem;\n line-height: 1.5rem;\n}\n.Chartbrowser-program {\n font-weight: normal;\n margin: 0 0 1rem 0;\n}\n.Chartbrowser-program:not(:first-of-type) {\n margin-top: 2rem;\n}\n.Chartbrowser-chart {\n background-color: var(--color-background);\n border: 1px solid transparent;\n margin-bottom: 1rem;\n padding: 0.875rem;\n box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);\n}\n.Chartbrowser-chart-name {\n text-align: center;\n margin: 0;\n}\n", "/*!\n * Copyright 2023 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n\n@import url(\"../shared/chartbrowser.css\");\n\n.Charts {\n margin-top: 1.5rem;\n}\n"],
|
||||
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE,wBCvCF,wBACE,6BAGF,mBACE,aACA,mBAEF,oBACE,SAlBF,qBAqBA,sBACE,gBACA,kBAvBF,iBA0BA,2BACE,gBACA,SACA,YAEF,8BACE,gBACA,SACA,aAEF,mBACE,+BACA,kBACA,mBAEF,sBACE,gBA1CF,gBA6CA,0CACE,gBAEF,oBACE,yCACA,6BACA,mBAnDF,gBAqDE,uDAEF,yBACE,kBAxDF,SCSA,QACE",
|
||||
"names": []
|
||||
}
|
||||
+74
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
+8
@@ -0,0 +1,8 @@
|
||||
/* Code generated by esbuild. DO NOT EDIT. */
|
||||
.js-Tree ul{list-style:none;padding-left:0}.js-Tree-item ul{display:none}.js-Tree-item{overflow:hidden;text-overflow:ellipsis;padding:.125rem 0}.js-Tree-item[aria-expanded=true] ul{display:block}.js-Tree-item .js-Tree-item{position:relative;padding-left:1.25rem}.js-Tree-item .js-Tree-item[aria-selected=true]:before{background-color:var(--color-brand-primary);border-radius:50%;content:"";display:block;height:.3125rem;left:.4688rem;position:absolute;top:.75rem;width:.3125rem}.js-Tree-item>a{color:var(--color-text-subtle);font-size:.875rem}.js-Tree-item[aria-selected=true]>a{color:var(--color-text)}svg g[aria-label=tip] g{fill:var(--color-background)}.Chartbrowser-view{display:flex;flex-direction:row}.Chartbrowser-index{flex:1 1;padding:0 1.5rem 0 0}.Chartbrowser-heading{font-weight:700;font-size:1.25rem;margin:0 0 .5rem}.Chartbrowser-index-sticky{position:sticky;top:1rem;width:10rem}.Chartbrowser-index-sticky>ul{position:sticky;top:1rem;margin-top:0}.Chartbrowser-link{color:var(--color-text-subtle);font-size:.875rem;line-height:1.5rem}.Chartbrowser-program{font-weight:400;margin:0 0 1rem}.Chartbrowser-program:not(:first-of-type){margin-top:2rem}.Chartbrowser-chart{background-color:var(--color-background);border:1px solid transparent;margin-bottom:1rem;padding:.875rem;box-shadow:0 1px 2px #3c40434d,0 1px 3px 1px #3c404326}.Chartbrowser-chart-name{text-align:center;margin:0}p{max-width:none}.Charts{margin-top:1rem}.Centered{display:flex;justify-content:center}.Charts-heading{margin:2.5rem 0 1.5rem}.Charts-heading h2{font-weight:400;margin:0}.Charts-heading a{border:var(--border);padding:.6rem;border-radius:.5rem;background-color:var(--color-background-highlighted-link)}.Charts-heading ul{margin:.5rem 0 0;list-style:none;display:inline-flex;padding:0}.Charts-heading li{list-style:none;display:inline-flex}.Charts-heading li:not(:last-child){margin-right:1rem}
|
||||
/*!
|
||||
* Copyright 2024 The Go Authors. All rights reserved.
|
||||
* Use of this source code is governed by a BSD-style
|
||||
* license that can be found in the LICENSE file.
|
||||
*/
|
||||
/*# sourceMappingURL=index.min.css.map */
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": ["../../shared/treenav.css", "../../shared/chartbrowser.css", "../index.css"],
|
||||
"sourcesContent": ["/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n.js-Tree ul {\n list-style: none;\n padding-left: 0;\n}\n\n.js-Tree-item ul {\n display: none;\n}\n\n.js-Tree-item {\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0.125rem 0 0.125rem 0;\n}\n\n.js-Tree-item[aria-expanded='true'] ul {\n display: block;\n}\n\n.js-Tree-item .js-Tree-item {\n position: relative;\n padding-left: 1.25rem;\n}\n\n.js-Tree-item .js-Tree-item[aria-selected='true']:before {\n background-color: var(--color-brand-primary);\n border-radius: 50%;\n content: \"\";\n display: block;\n height: .3125rem;\n left: .4688rem;\n position: absolute;\n top: .75rem;\n width: .3125rem;\n}\n\n.js-Tree-item>a {\n color: var(--color-text-subtle);\n font-size: .875rem;\n}\n\n.js-Tree-item[aria-selected='true']>a {\n color: var(--color-text);\n}\n\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/treenav.css\");\n\n/* Fix tooltip background for dark theme */\nsvg g[aria-label=\"tip\"] g {\n fill: var(--color-background);\n}\n\n.Chartbrowser-view {\n display: flex;\n flex-direction: row;\n}\n.Chartbrowser-index {\n flex: 1 1;\n padding: 0 1.5rem 0 0;\n}\n.Chartbrowser-heading {\n font-weight: bold;\n font-size: 1.25rem;\n margin: 0 0 0.5rem 0;\n}\n.Chartbrowser-index-sticky {\n position: sticky;\n top: 1rem;\n width: 10rem;\n}\n.Chartbrowser-index-sticky > ul {\n position: sticky;\n top: 1rem;\n margin-top: 0;\n}\n.Chartbrowser-link {\n color: var(--color-text-subtle);\n font-size: .875rem;\n line-height: 1.5rem;\n}\n.Chartbrowser-program {\n font-weight: normal;\n margin: 0 0 1rem 0;\n}\n.Chartbrowser-program:not(:first-of-type) {\n margin-top: 2rem;\n}\n.Chartbrowser-chart {\n background-color: var(--color-background);\n border: 1px solid transparent;\n margin-bottom: 1rem;\n padding: 0.875rem;\n box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .3), 0 1px 3px 1px rgba(60, 64, 67, .15);\n}\n.Chartbrowser-chart-name {\n text-align: center;\n margin: 0;\n}\n", "/*!\n * Copyright 2024 The Go Authors. All rights reserved.\n * Use of this source code is governed by a BSD-style\n * license that can be found in the LICENSE file.\n */\n\n@import url(\"../shared/chartbrowser.css\");\n\np {\n /* Reset from _typography.css */\n max-width: none;\n}\n\n.Charts {\n margin-top: 1rem;\n}\n.Centered {\n display: flex;\n justify-content: center;\n}\n.Charts-heading {\n margin: 2.5rem 0 1.5rem;\n}\n.Charts-heading h2 {\n font-weight: normal;\n margin: 0;\n}\n.Charts-heading a {\n border: var(--border);\n padding: 0.6rem;\n border-radius: 0.5rem;\n background-color: var(--color-background-highlighted-link);\n}\n.Charts-heading ul {\n margin: 0.5rem 0 0 0;\n list-style: none;\n display: inline-flex;\n padding: 0;\n}\n.Charts-heading li {\n list-style: none;\n display: inline-flex;\n}\n.Charts-heading li:not(:last-child) {\n margin-right: 1rem;\n}\n"],
|
||||
"mappings": ";AAMA,YACE,gBACA,eAGF,iBACE,aAGF,cACE,gBACA,uBAjBF,kBAqBA,qCACE,cAGF,4BACE,kBACA,qBAGF,uDACE,4CA/BF,kBAiCE,WACA,cACA,gBACA,cACA,kBACA,WACA,eAGF,gBACE,+BACA,kBAGF,oCACE,wBCvCF,wBACE,6BAGF,mBACE,aACA,mBAEF,oBACE,SAlBF,qBAqBA,sBACE,gBACA,kBAvBF,iBA0BA,2BACE,gBACA,SACA,YAEF,8BACE,gBACA,SACA,aAEF,mBACE,+BACA,kBACA,mBAEF,sBACE,gBA1CF,gBA6CA,0CACE,gBAEF,oBACE,yCACA,6BACA,mBAnDF,gBAqDE,uDAEF,yBACE,kBAxDF,SCQA,EAEE,eAGF,QACE,gBAEF,UACE,aACA,uBAEF,gBApBA,uBAuBA,mBACE,gBAxBF,SA2BA,kBACI,qBA5BJ,kCA+BI,0DAEJ,mBAjCA,iBAmCE,gBACA,oBApCF,UAuCA,mBACE,gBACA,oBAEF,oCACE",
|
||||
"names": []
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user