Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tinygo support, goroutine hack change, fix panic for invalid stderr, add test #66

Merged
merged 12 commits into from
Jul 19, 2024
15 changes: 14 additions & 1 deletion console_logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,13 +88,26 @@ var (
Color = false
)

// Is the file (e.g os.StdErr) Stat()able so we can detect if it's a tty or not.
// If not we switch in init() to Stdout.
func isValid(file *os.File) bool {
if file == nil {
return false
}
_, err := file.Stat()
return err == nil
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
}

// ConsoleLogging is a utility to check if the current logger output is a console (terminal).
func ConsoleLogging() bool {
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
f, ok := jWriter.w.(*os.File)
if !ok {
return false
}
s, _ := f.Stat()
s, err := f.Stat()
if err != nil {
return false
}
return (s.Mode() & os.ModeCharDevice) == os.ModeCharDevice
}

Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module fortio.org/log

go 1.18

require fortio.org/struct2env v0.4.1
require (
fortio.org/struct2env v0.4.1
github.com/kortschak/goroutine v1.1.2
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
fortio.org/struct2env v0.4.1 h1:rJludAMO5eBvpWplWEQNqoVDFZr4RWMQX7RUapgZyc0=
fortio.org/struct2env v0.4.1/go.mod h1:lENUe70UwA1zDUCX+8AsO663QCFqYaprk5lnPhjD410=
github.com/kortschak/goroutine v1.1.2 h1:lhllcCuERxMIK5cYr8yohZZScL1na+JM5JYPRclWjck=
github.com/kortschak/goroutine v1.1.2/go.mod h1:zKpXs1FWN/6mXasDQzfl7g0LrGFIOiA6cLs9eXKyaMY=
48 changes: 5 additions & 43 deletions goroutine/gid.go
Original file line number Diff line number Diff line change
@@ -1,52 +1,14 @@
// Copyright ©2020 Dan Kortschak. 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 !tinygo

// Package goroutine provides a single function that will return the runtime's
// ID number for the calling goroutine.
//
// The implementation is derived from Laevus Dexter's comment in Gophers' Slack #darkarts,
// https://gophers.slack.com/archives/C1C1YSQBT/p1593885226448300 post which linked to
// this playground snippet https://play.golang.org/p/CSOp9wyzydP.
package goroutine

import (
"reflect"
"unsafe"
"github.com/kortschak/goroutine" // Rely on and forward to the original rather than maintain our own copy.
)

const IsTinyGo = false

// ID returns the runtime ID of the calling goroutine.
func ID() int64 {
return *(*int64)(add(getg(), goidoff))
}

//go:nosplit
func getg() unsafe.Pointer {
return *(*unsafe.Pointer)(add(getm(), curgoff))
return goroutine.ID()
}

//go:linkname add runtime.add
//go:nosplit
func add(p unsafe.Pointer, x uintptr) unsafe.Pointer

//go:linkname getm runtime.getm
//go:nosplit
func getm() unsafe.Pointer

var (
curgoff = offset("*runtime.m", "curg")
goidoff = offset("*runtime.g", "goid")
)

// offset returns the offset into typ for the given field.
func offset(typ, field string) uintptr {
rt := toType(typesByString(typ)[0])
f, _ := rt.Elem().FieldByName(field)
return f.Offset
}

//go:linkname typesByString reflect.typesByString
func typesByString(s string) []unsafe.Pointer

//go:linkname toType reflect.toType
func toType(t unsafe.Pointer) reflect.Type
16 changes: 14 additions & 2 deletions goroutine/gid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
)

Expand All @@ -19,8 +20,12 @@ func TestID(t *testing.T) {
if got != want {
t.Fatalf("unexpected id for main goroutine: got:%d want:%d", got, want)
}
n := 1000000 // for regular go
if IsTinyGo {
n = 1000 // for tinygo, it OOMs with 1000000 and we're only self testing that we get different increasing ids.
}
var wg sync.WaitGroup
for i := 0; i < 1000000; i++ {
for i := 0; i < n; i++ {
i := i
wg.Add(1)
go func() {
Expand All @@ -35,8 +40,14 @@ func TestID(t *testing.T) {
wg.Wait()
}

var testID int64

// goid returns the goroutine ID extracted from a stack trace.
func goid() int64 {
if IsTinyGo {
// pretty horrible test that aligns with the implementation, but at least it tests we get 1,2,3... different numbers.
return atomic.AddInt64(&testID, 1)
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
}
var buf [64]byte
n := runtime.Stack(buf[:], false)
idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0]
Expand All @@ -47,8 +58,9 @@ func goid() int64 {
return id
}

var gotid int64 // outside of the function to help avoiding compiler optimizations

func BenchmarkGID(b *testing.B) {
var gotid int64
for n := 0; n < b.N; n++ {
gotid += ID()
}
Expand Down
39 changes: 39 additions & 0 deletions goroutine/gid_tinygo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//go:build tinygo

package goroutine

import (
"sync"
"unsafe"
)

const IsTinyGo = true

var (
counter int64
mapping = make(map[uintptr]int64)
// TinyGo at the moment is single threaded, so this is not needed, but it's good to have anyway
// in case that changes. It does add ~5ns (from 20ns vs 4ns big go) but it's better to be correct.
// In theory, the mutex could be noop on platforms where everything is single threaded.
lock sync.Mutex
)

func ID() int64 {
task := uintptr(currentTask())
lock.Lock() // explicit minimal critical section without using defer, on purpose.
if id, ok := mapping[task]; ok {
lock.Unlock()
return id
}
counter++
mapping[task] = counter
lock.Unlock()
return counter
// or, super fast but ugly large numbers/pointers:
//return int64(uintptr(currentTask()))
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
}

// Call https://github.com/tinygo-org/tinygo/blob/v0.32.0/src/internal/task/task_stack.go#L39
//
//go:linkname currentTask internal/task.Current
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
func currentTask() unsafe.Pointer
3 changes: 3 additions & 0 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ func (l *JSONEntry) Time() time.Time {

//nolint:gochecknoinits // needed
func init() {
if !isValid(os.Stderr) { // wasm in browser case for instance
SetOutput(os.Stdout) // this could also be invalid too, but... we tried.
}
setLevel(Info) // starting value
levelToStrM = make(map[string]Level, 2*len(LevelToStrA))
JSONStringLevelToLevel = make(map[string]Level, len(LevelToJSON)-1) // -1 to not reverse info to NoLevel
Expand Down
21 changes: 20 additions & 1 deletion logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,26 @@ func TestConfigFromEnvOk(t *testing.T) {
}
}

// io.Discard but specially known to by logger optimizations for instance.
func TestInvalidFile(t *testing.T) {
if isValid(nil) {
t.Errorf("expected nil to be invalid")
}
prev := jWriter.w
invalidFile := os.NewFile(^uintptr(0), "invalid-file")
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
jWriter.w = invalidFile
b := ConsoleLogging()
ldemailly marked this conversation as resolved.
Show resolved Hide resolved
jWriter.w = prev
if b {
t.Errorf("expected not to be console logging")
}
}

// --- Benchmarks

// This `discard` is like io.Discard, except that io.Discard is checked explicitly
// (e.g. https://cs.opensource.google/go/go/+/refs/tags/go1.22.5:src/log/log.go;l=84)
// in logger optimizations and we want to measure the actual production
// of messages.
type discard struct{}

func (discard) Write(p []byte) (int, error) {
Expand Down
Loading