Enabling USB consumes a lot of power. So it's better to keep it disabled
by default when the serial port is set to something other than "usb"
(USB-CDC).
On my nice!nano clone, it reduces current consumption from 1mA to
0.13mA (and most of the rest is probably consumed by a connected SCD41
sensor).
This change might break some expectations, when a board uses USB but not
USB-CDC (I think this is rare, but I didn't check specifically).
The setjmp code sets r24 to 0 and checks if it is still 0 when it finishes.
This usually works because tinygo_longjmp stores the lower half of the stack pointer into r24.
However, r24 will be set to 0 if the stack is aligned to 256 bytes when entering the setjmp.
Manually set r24 to 1 in tinygo_longjmp to fix this issue.
Loop over valid pointer locations in heap objects instead of checking if each location is valid.
The conservative scanning code is now shared between markRoots and the heap scan.
This also removes the ending alignment requirement from markRoots, since the new scan* functions do not require an aligned length.
This requirement was occasionally violated by the linux global marking code.
This saves some code space and has negligible impact on performance.
The blocks GC originally used a fixed-size stack to hold objects to scan.
When this stack overflowed, the GC would fully rescan all marked objects.
This could cause the GC to degrade to O(n^2) when scanning large linked data structures.
Instead of using a fixed-size stack, we now add a pointer field to the start of each object.
This pointer field is used to implement an unbounded linked stack.
This also consolidates the heap object scanning into one place, which simplifies the process.
This comes at the cost of introducing a pointer field to the start of the object, plus the cost of aligning the result.
This translates to:
- 16 bytes of overhead on x86/arm64 with the conservative collector
- 0 bytes of overhead on x86/arm64 with the precise collector (the layout field cost gets aligned up to 16 bytes anyway)
- 8 bytes of overhead on other 64-bit systems
- 4 bytes of overhead on 32-bit systems
- 2 bytes of overhead on AVR
In order to scan stacks, the GC preempts all other threads and has them scan their own stack.
This is somewhat expensive since all of these threads have to fight over a single lock.
Instead, save the stack bounds and let the GC thread perform the scan.
This also fixes a few other bugs I ran into:
1. The GC starts scanning before the world stops. This can cause it to miss some objects (and mistakenly free them) if memory is modified while stopping.
2. The GC does not wait for threads to resume. This can cause notifications to be misinterpreted due to signal nesting if the GC is re-run before all threads wake.
This fixes a bug where runtime.alloc would not clear the padding from rounding the allocation up to a multiple of the block size.
The GC still has to scan this, and this could result in permanent false positives (and therefore memory leaks).
It also handles overflow in the size calculation.
This fixes the "gc: world already stopped" check by moving it before gcMarkReachable.
Previously the check did not work because gcMarkReachable would hang while waiting for activeTaskLock.
- Do not use make([]byte, ...) to allocate, instead call the allocator
directly with a nil (undefined) layout. This makes sure the precise
GC will scan the contents of the allocation, since C could very well
put pointers in there.
- Simplify the map to use the pointer as the key and the size as the
value, instead of storing the slices directly in the map.
Long sleep durations weren't implemented correctly, by simply casting to
a smaller number of bits. What we want is a saturating downcast, which
is what I've used here instead.
This should fix https://github.com/tinygo-org/tinygo/issues/3356.
This function is called when a hard fault occurs. Hard faults happen
when something really bad happens - like writing to unwritable memory or
an unaligned memory access on Cortex-M0. It is not generally possible to
recover from these.
This commit optimizes the code size overhead of hard fault handling:
* It removes the stack overflow checking code.
This may seem like a bad thing, but the only thing this could check
were stack overflows outside goroutines. In practice, this could
only really happen on a stack overflow in the scheduler (unlikely),
or in interrupt code (possible, but interrupts are small so still
unlikely). Most stack overflows happen in regular goroutines, and
weren't caught in the HardFault.
* It makes the panic message similar to a regular panic. This has two
advantages:
* It reduces code size, because the string can be reused between
the HardFault handler and the runtime panic function.
* Using the same pattern automatically makes `-monitor` print the
source address for the hard fault. Not a big benefit as we could
trivially add any other pattern but a nice benefit nonetheless.
Result:
$ tinygo flash -target=microbit -size=short -programmer=openocd -monitor examples/serial
code data bss | flash ram
3036 8 2256 | 3044 2264
[...snip]
Connected to /dev/ttyACM0. Press Ctrl-C to exit.
panic: runtime error at 0x00000344: HardFault with sp=0x200007d0
[tinygo: panic at /home/ayke/src/tinygo/tinygo/src/internal/task/task_stack_cortexm.go:48:4]
(This is with https://github.com/tinygo-org/tinygo/pull/3680 not yet
fixed and some local changes to configure the UART so I can actually see
the panic).
For atsamd21/nrf51 chips this results in a binary size reduction of
around 100 bytes. For other Cortex-M chips it's around 24 bytes but I
hope to change this in the future because a lot of the fault decoding in
runtime_cortexm_hardfault_debug.go should IMHO be done by the TinyGo
monitor instead (I estimate that this would save around 800 bytes on
these chips).
Account for the sleep queue base time in the computation of the wakeup
time.
Tested with the following program on pico2.
func main() {
go func() {
for i := range 60 {
const delay = 20 * time.Millisecond
before := time.Now()
time.Sleep(delay)
if d := time.Since(before); true || d < delay {
log.Println(i, "actual", d, "delay", delay)
}
}
}()
time.Sleep(500 * time.Millisecond)
log.Println("******** done sleeping ********")
select {}
}
Without this change, the program would print lines such as:
17 actual 15.494ms delay 20ms
18 actual 15.49ms delay 20ms
19 actual 15.585ms delay 20ms
20 actual 15.493ms delay 20ms
21 actual 15.494ms delay 20ms
22 actual 15.487ms delay 20ms
23 actual 15.498ms delay 20ms
******** done sleeping ********
24 actual 15.548ms delay 20ms
25 actual 20.011ms delay 20ms
26 actual 20.01ms delay 20ms
27 actual 20.011ms delay 20ms
28 actual 20.015ms delay 20ms
Note that while more than one sleeping goroutine is in the timer queue,
the sleep duration is 5ms short.
The previous versions calculated at init() prevented `interp` from running
in many cases, increasing compile times due to the increased need to revert
the partially interpreted results and also increasing binary runtime because
fewer optimizations had happened during interp.
This should avoid a deadlock when trying to print inside an interrupt,
if the interrupted code is also printing (and therefore has the print
lock taken).
The GC shouldn't try to interrupt other cores before they are started.
For example, it would be possible for the GC to run in a package
initializer (which is currently run on a single core). That would
suggest questionable program design, but it is something that should
work. So this commit makes sure the GC only tries to scan the stack of
other cores when those other cores have in fact started.
This adds support for `-gc=boehm` on `-target=wasip1` and `-target=wasm`
(in a browser or NodeJS). Notably it does *not* add Boehm GC support for
`-target=wasip2`, since that target doesn't have a real libc.
This commit adds support for a scheduler that runs a scheduler on all
available cores. It is meant to be used on baremetal systems with a
fixed number of cores, such as the RP2040.
The initial implementation adds support for multicore scheduling to the
riscv-qemu target as a convenient testing target. This means that this
new multicore scheduler is tested in CI, including a bunch of standard
library tests (`make tinygo-test-baremetal`). This should ensure the new
scheduler is reasonably well tested before trying to use it on
harder-to-debug targets like the RP2040.
The system stack is only needed when we're not on it. So we can directly
call task.SystemStack() without problems.
This also saves a tiny bit of binary size.
This type can be used to jump back to a previous position in a program
from inside an interrupt. This is useful for baremetal systems that
implement wfi but not wfe, and therefore have no easy (race-free) way to
wait until a flag gets changed inside an interrupt. This is an issue on
RISC-V, where this is racy (the interrupt might happen after the check
but before the wfi instruction):
configureInterrupt()
for flag.Load() != 0 {
riscv.Asm("wfi")
}
This commit changes signal handling in a few ways:
* It stubs signals for all wasm targets (not just wasi) and baremetal,
since none of those have traditional POSIX signals. And moves the
code for that into a single file, instead of duplicating it.
* It removes the stub for signal_ignored since the value `false` might
be wrong in some cases and it doesn't usually seem to be called (it
is not called in tsgo). Should be trivial to re-add if it is shown
to be needed.
* It adds a stub for `os/signal.signalWaitUntilIdle` which _is_ called
by tsgo.
Without this change, a pending interrupt would spuriously trigger
immediately after enabling. This happens if an interrupt is triggered
during flashing (e.g. by DMA), which survives the subsequent reset.
This behaviour matches e.g. `machine.irqSet` in machine_rp2_rp2350.go.
See 7f970a45, whose symptoms were likely caused by spurious interrupts.
For the threads scheduler, it makes sense to have NumCPU available.
For all other schedulers, the number of available CPUs is practically
limited to one by the scheduler (even though the system might have more
CPUs).
This variable is only necessary on the cooperative and none scheduler.
It is not used on the threads scheduler.
The reason for moving is that the upcoming multicore baremetal scheduler
also needs mainExited but of a different type: an atomic variable
instead of a plain boolean.
This is not a scheduler in the runtime, instead every goroutine is
mapped to a single OS thread - meaning 1:1 scheduling.
While this may not perform well (or at all) for large numbers of
threads, it greatly simplifies many things in the runtime. For example,
blocking syscalls can be called directly instead of having to use epoll
or similar. Also, we don't need to do anything special to call C code -
the default stack is all we need.
Using a global lock may be slow, but it is certainly simple and safe.
If this global lock becomes a bottleneck, we can of course look into
making the GC truly support multithreading.