608 lines
19 KiB
C
608 lines
19 KiB
C
/*
|
||
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
|
||
*
|
||
* SPDX-License-Identifier: Apache-2.0
|
||
*/
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <stdint.h>
|
||
#include <unistd.h>
|
||
#include <pthread.h>
|
||
#include <sys/queue.h>
|
||
#include <time.h>
|
||
#include "string.h"
|
||
#include "FreeRTOS.h"
|
||
#include "task.h"
|
||
#include "utils/wait_for_event.h"
|
||
#include "esp_private/freertos_linux_coop_syscalls.h"
|
||
#include "utils/linux_port_utils.h"
|
||
|
||
#define FREERTOS_SIM_TICK_PERIOD_US (1000000 / CONFIG_FREERTOS_HZ)
|
||
|
||
typedef struct thread {
|
||
const char *name;
|
||
pthread_t pthread;
|
||
TaskFunction_t pxCode;
|
||
void *pvParams;
|
||
bool is_dying;
|
||
bool yield_needed;
|
||
struct event *ev;
|
||
} thread_t;
|
||
|
||
typedef struct task_thread_node {
|
||
TaskHandle_t handle;
|
||
thread_t *thread;
|
||
SLIST_ENTRY(task_thread_node) next;
|
||
} task_thread_node_t;
|
||
|
||
|
||
static SLIST_HEAD(task_thread_node_ll, task_thread_node) s_task_thread_list = SLIST_HEAD_INITIALIZER(task_thread_node);
|
||
static pthread_mutex_t s_thread_map_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||
|
||
static pthread_mutex_t s_port_mutex;
|
||
static pthread_t s_scheduler_thread;
|
||
static bool s_scheduler_started = false;
|
||
static int s_ux_critical_nesting = 0;
|
||
|
||
/* TLS flag: true only when inside a real FreeRTOS task pthread */
|
||
static __thread bool s_in_freertos_task = false;
|
||
|
||
bool linux_port_in_freertos_task(void)
|
||
{
|
||
return s_in_freertos_task;
|
||
}
|
||
|
||
static void linux_port_initialize_mutexes(void)
|
||
{
|
||
pthread_mutexattr_t attr;
|
||
pthread_mutexattr_init(&attr);
|
||
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
|
||
pthread_mutex_init(&s_port_mutex, &attr);
|
||
pthread_mutexattr_destroy(&attr);
|
||
}
|
||
|
||
static void linux_port_fatal_error(const char *msg, int err)
|
||
{
|
||
fprintf(stderr, "%s: %s\n", msg, strerror(err));
|
||
abort();
|
||
}
|
||
|
||
static void linux_port_register_thread(TaskHandle_t handle, thread_t *thread)
|
||
{
|
||
if (handle == NULL) {
|
||
return;
|
||
}
|
||
|
||
task_thread_node_t *node = malloc(sizeof(task_thread_node_t));
|
||
if (!node) {
|
||
linux_port_fatal_error("Failed to allocate thread map node", -1);
|
||
}
|
||
|
||
node->handle = handle;
|
||
node->thread = thread;
|
||
|
||
pthread_mutex_lock(&s_thread_map_mutex);
|
||
SLIST_INSERT_HEAD(&s_task_thread_list, node, next);
|
||
pthread_mutex_unlock(&s_thread_map_mutex);
|
||
}
|
||
|
||
static void linux_port_unregister_thread(TaskHandle_t handle)
|
||
{
|
||
if (handle == NULL) {
|
||
return;
|
||
}
|
||
|
||
pthread_mutex_lock(&s_thread_map_mutex);
|
||
|
||
task_thread_node_t *cur_node = SLIST_FIRST(&s_task_thread_list);
|
||
task_thread_node_t *prev_node = NULL;
|
||
|
||
while (cur_node) {
|
||
if (cur_node->handle == handle) {
|
||
if (prev_node) {
|
||
prev_node->next.sle_next = SLIST_NEXT(cur_node, next);
|
||
} else {
|
||
SLIST_REMOVE_HEAD(&s_task_thread_list, next);
|
||
}
|
||
|
||
free(cur_node);
|
||
pthread_mutex_unlock(&s_thread_map_mutex);
|
||
return;
|
||
}
|
||
|
||
prev_node = cur_node;
|
||
cur_node = SLIST_NEXT(cur_node, next);
|
||
}
|
||
|
||
pthread_mutex_unlock(&s_thread_map_mutex);
|
||
}
|
||
|
||
static thread_t *linux_port_get_thread_from_handle(TaskHandle_t handle)
|
||
{
|
||
if (handle == NULL) {
|
||
return NULL;
|
||
}
|
||
|
||
pthread_mutex_lock(&s_thread_map_mutex);
|
||
task_thread_node_t *node = NULL;
|
||
SLIST_FOREACH(node, &s_task_thread_list, next) {
|
||
if (node->handle == (TaskHandle_t)(*(StackType_t **)(handle))) {
|
||
thread_t *t = node->thread;
|
||
pthread_mutex_unlock(&s_thread_map_mutex);
|
||
return t;
|
||
}
|
||
}
|
||
pthread_mutex_unlock(&s_thread_map_mutex);
|
||
return NULL;
|
||
}
|
||
|
||
static thread_t *linux_port_get_calling_thread(void)
|
||
{
|
||
pthread_t self = pthread_self();
|
||
pthread_mutex_lock(&s_thread_map_mutex);
|
||
task_thread_node_t *node = NULL;
|
||
SLIST_FOREACH(node, &s_task_thread_list, next) {
|
||
if (pthread_equal(node->thread->pthread, self)) {
|
||
thread_t *thread = node->thread;
|
||
pthread_mutex_unlock(&s_thread_map_mutex);
|
||
return thread;
|
||
}
|
||
}
|
||
pthread_mutex_unlock(&s_thread_map_mutex);
|
||
return NULL;
|
||
}
|
||
|
||
pthread_t linux_port_get_scheduled_task_pthread(void)
|
||
{
|
||
thread_t *thread = linux_port_get_thread_from_handle(xTaskGetCurrentTaskHandle());
|
||
return thread ? thread->pthread : pthread_self();
|
||
}
|
||
|
||
static void *linux_port_task_runner(void *arg)
|
||
{
|
||
/* Allow this thread to be cancelled */
|
||
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
|
||
|
||
/* set the flag showing that this is a freertos task */
|
||
s_in_freertos_task = true;
|
||
|
||
/* setup the backtrace signal. ONLY triggered before abort so
|
||
* it will not interfere with the simulation while its running */
|
||
linux_port_setup_backtrace_signal();
|
||
|
||
thread_t *thread = arg;
|
||
|
||
/* Block until scheduler signals first time, then run the task body. */
|
||
event_wait(thread->ev);
|
||
thread->pxCode(thread->pvParams);
|
||
|
||
return NULL;
|
||
}
|
||
|
||
static void linux_port_unblock_thread(thread_t *thread)
|
||
{
|
||
event_signal(thread->ev);
|
||
}
|
||
|
||
static void linux_port_block_thread(thread_t *thread)
|
||
{
|
||
event_wait(thread->ev);
|
||
}
|
||
|
||
static void linux_port_increment_tick(void)
|
||
{
|
||
(void)xTaskIncrementTick();
|
||
}
|
||
|
||
static void linux_port_switch_context(TaskHandle_t current_task_hdl)
|
||
{
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
|
||
thread_t *current_thread = linux_port_get_thread_from_handle(current_task_hdl);
|
||
|
||
/* get the task that should be scheduled next */
|
||
vTaskSwitchContext();
|
||
|
||
/* get the new task to schedule and the associated thread item */
|
||
TaskHandle_t next_task_hdl = xTaskGetCurrentTaskHandle();
|
||
thread_t *next_thread = linux_port_get_thread_from_handle(next_task_hdl);
|
||
|
||
/* unblock the newly scheduled task if it is different from
|
||
* the one already scheduled */
|
||
if (next_thread) {
|
||
/* Only unblock the thread if we are actually switching to a
|
||
* different one. Signaling the already-running thread would
|
||
* leave a stale event_triggered flag, causing its next
|
||
* event_wait (e.g. in vPortYield) to return immediately
|
||
* instead of blocking.
|
||
*
|
||
* Exception: on the very first switch, the task is still blocked
|
||
* in its initial event_wait (linux_port_task_runner), so we must
|
||
* signal it even though current_thread == next_thread. */
|
||
if (next_thread != current_thread || !s_scheduler_started) {
|
||
/* Discard any stale wake on the task being switched out.
|
||
*
|
||
* Because tasks are pthreads that cannot be forcibly paused,
|
||
* the port can only "unschedule" a task by choosing a different
|
||
* next task; the outgoing task keeps running until it next
|
||
* blocks itself in event_wait (e.g. via vPortYield). If, while
|
||
* it was unscheduled, the scheduler already switched back into
|
||
* it (event_signal in a later switch) without the task ever
|
||
* having parked in event_wait, that signal is never consumed
|
||
* and event_triggered stays latched. Its next voluntary block
|
||
* would then return immediately instead of blocking (e.g.
|
||
* vTaskDelay(100ms) returning in 0ms).
|
||
*
|
||
* Clearing here, under s_port_mutex and atomically with the
|
||
* scheduling decision, drops that latched-but-unconsumed wake.
|
||
* It is safe: a legitimate wake can only be delivered later,
|
||
* when the scheduler next selects this task as next_thread.
|
||
*
|
||
* Reached only for a genuine switch to a different task:
|
||
* entering this block with next_thread == current_thread
|
||
* happens solely on the first switch (!s_scheduler_started),
|
||
* which the current_thread NULL-check below also tolerates.
|
||
* The NULL-check additionally guards the deleted-task path
|
||
* where the outgoing task has no thread mapping. */
|
||
if (current_thread) {
|
||
event_clear(current_thread->ev);
|
||
}
|
||
linux_port_unblock_thread(next_thread);
|
||
}
|
||
}
|
||
|
||
if (!s_scheduler_started) {
|
||
s_scheduler_started = true;
|
||
}
|
||
|
||
/* fill the name of the task in the thread item if not done already. */
|
||
if (next_thread && next_thread->name == NULL) {
|
||
/* fill the name of the thread now */
|
||
next_thread->name = pcTaskGetName(next_task_hdl);
|
||
}
|
||
|
||
/* fill the name of the task in the thread item if not done already. */
|
||
if (current_thread && current_thread->name == NULL) {
|
||
/* fill the name of the thread now */
|
||
current_thread->name = pcTaskGetName(current_task_hdl);
|
||
}
|
||
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
}
|
||
|
||
static void *linux_port_scheduler_runner(void *arg)
|
||
{
|
||
(void)arg;
|
||
|
||
while (1) {
|
||
/* sleep for a period of 1 tick */
|
||
usleep(FREERTOS_SIM_TICK_PERIOD_US);
|
||
|
||
/* get the task that is currently scheduled */
|
||
TaskHandle_t current_task_hdl = xTaskGetCurrentTaskHandle();
|
||
|
||
/* Lock the port mutex. This will block while any task is in a
|
||
* critical section, ensuring ticks don't preempt critical code. */
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
|
||
/* increment the freertos tick */
|
||
linux_port_increment_tick();
|
||
|
||
/* schedule a new task, and schedule out the currently running one */
|
||
linux_port_switch_context(current_task_hdl);
|
||
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
}
|
||
return NULL;
|
||
}
|
||
|
||
StackType_t *pxPortInitialiseStack(StackType_t *pxTopOfStack,
|
||
StackType_t *pxEndOfStack,
|
||
TaskFunction_t pxCode,
|
||
void *pvParameters)
|
||
{
|
||
pthread_attr_t thread_attr;
|
||
size_t thread_stack_size;
|
||
|
||
/* Store the thread data at the start of the stack. */
|
||
thread_stack_size = (pxTopOfStack - pxEndOfStack) * sizeof(*pxTopOfStack);
|
||
pthread_attr_init(&thread_attr);
|
||
pthread_attr_setstack(&thread_attr, pxEndOfStack, thread_stack_size);
|
||
|
||
thread_t *thread = malloc(sizeof(thread_t));
|
||
if (!thread) {
|
||
linux_port_fatal_error("Failed to allocate thread metadata", -1);
|
||
}
|
||
thread->name = NULL; // this will be filled later when we know about the task name
|
||
thread->pxCode = pxCode;
|
||
thread->pvParams = pvParameters;
|
||
thread->is_dying = false;
|
||
thread->yield_needed = false;
|
||
thread->ev = event_create();
|
||
|
||
linux_port_register_thread((TaskHandle_t)pxTopOfStack, thread);
|
||
|
||
/* create the thread associated with the task being created */
|
||
const int ret = pthread_create(&thread->pthread, &thread_attr, linux_port_task_runner, thread);
|
||
if (ret != 0) {
|
||
linux_port_fatal_error("pthread_create", ret);
|
||
}
|
||
return pxTopOfStack;
|
||
}
|
||
|
||
BaseType_t xPortStartScheduler(void)
|
||
{
|
||
/* set the port mutex to be recursive. Must be done before
|
||
* vPortEnableInterrupts() which calls vPortExitCritical(). */
|
||
linux_port_initialize_mutexes();
|
||
|
||
/* enable interrupt that were disabled in vTaskStartScheduler */
|
||
vPortEnableInterrupts();
|
||
|
||
/* init the cooperative syscall layer (sets stdio non-blocking).
|
||
* Provided by VFS component; weak no-op when VFS is not linked. */
|
||
freertos_linux_coop_syscalls_init();
|
||
|
||
/* Start scheduler thread */
|
||
int ret = pthread_create(&s_scheduler_thread, NULL, linux_port_scheduler_runner, NULL);
|
||
if (ret != 0) {
|
||
linux_port_fatal_error("pthread_create", ret);
|
||
}
|
||
|
||
/* Should never return */
|
||
pthread_join(s_scheduler_thread, NULL);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void vPortEndScheduler(void)
|
||
{
|
||
exit(0);
|
||
}
|
||
|
||
void vPortEnterCritical(void)
|
||
{
|
||
if (!s_scheduler_started) {
|
||
return;
|
||
}
|
||
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
|
||
/* Non-FreeRTOS thread or recursive enter: just bump the counter.
|
||
* The mutex is already held (recursive lock succeeds for same thread). */
|
||
if (!linux_port_in_freertos_task() || s_ux_critical_nesting > 0) {
|
||
s_ux_critical_nesting++;
|
||
return;
|
||
}
|
||
|
||
/* First enter from a FreeRTOS task: ensure we're the scheduled task.
|
||
* If not, release the mutex, block until the scheduler switches to us,
|
||
* then re-acquire. */
|
||
thread_t *calling_thread = linux_port_get_calling_thread();
|
||
thread_t *scheduled_thread = linux_port_get_thread_from_handle(xTaskGetCurrentTaskHandle());
|
||
|
||
while (calling_thread && !calling_thread->is_dying && calling_thread != scheduled_thread) {
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
linux_port_block_thread(calling_thread);
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
calling_thread = linux_port_get_calling_thread();
|
||
scheduled_thread = linux_port_get_thread_from_handle(xTaskGetCurrentTaskHandle());
|
||
}
|
||
|
||
if (!calling_thread || calling_thread->is_dying) {
|
||
linux_port_switch_context(xTaskGetCurrentTaskHandle());
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
return;
|
||
}
|
||
|
||
s_ux_critical_nesting = 1;
|
||
}
|
||
|
||
void vPortExitCritical(void)
|
||
{
|
||
if (!s_scheduler_started || s_ux_critical_nesting == 0) {
|
||
return;
|
||
}
|
||
|
||
s_ux_critical_nesting--;
|
||
|
||
/* Check for deferred yield on final exit from a FreeRTOS task */
|
||
if (s_ux_critical_nesting == 0 && linux_port_in_freertos_task()) {
|
||
thread_t *calling_thread = linux_port_get_calling_thread();
|
||
if (calling_thread && calling_thread->yield_needed) {
|
||
calling_thread->yield_needed = false;
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
vPortYield();
|
||
return;
|
||
}
|
||
}
|
||
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
}
|
||
|
||
/* Handle the case where the calling pthread is not a registered FreeRTOS task.
|
||
* If the calling thread has been deleted but is still the scheduled task,
|
||
* perform a context switch. Otherwise just release the mutex and return.
|
||
* Returns true if the caller should return early. */
|
||
static bool linux_port_handle_deleted_task(TaskHandle_t scheduled_task_hdl,
|
||
thread_t *calling_thread)
|
||
{
|
||
if (calling_thread != NULL) {
|
||
return false;
|
||
}
|
||
|
||
thread_t *scheduled_thread = linux_port_get_thread_from_handle(scheduled_task_hdl);
|
||
if (scheduled_thread == NULL) {
|
||
linux_port_switch_context(scheduled_task_hdl);
|
||
}
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
return true;
|
||
}
|
||
|
||
void vPortYield(void)
|
||
{
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
|
||
thread_t *calling_thread = linux_port_get_calling_thread();
|
||
TaskHandle_t scheduled_task_hdl = xTaskGetCurrentTaskHandle();
|
||
|
||
if (linux_port_handle_deleted_task(scheduled_task_hdl, calling_thread)) {
|
||
return;
|
||
}
|
||
|
||
/* If in a critical section, defer the yield until the section exits. */
|
||
if (s_ux_critical_nesting != 0) {
|
||
calling_thread->yield_needed = true;
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
return;
|
||
}
|
||
|
||
/* Hand the CPU to the next ready task right now (mimics PendSV on real
|
||
* hardware). */
|
||
linux_port_switch_context(scheduled_task_hdl);
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
|
||
/* If the newly scheduled task is different from the calling thread,
|
||
* block until the scheduler resumes this task. */
|
||
TaskHandle_t next_task_hdl = xTaskGetCurrentTaskHandle();
|
||
thread_t *next_thread = linux_port_get_thread_from_handle(next_task_hdl);
|
||
if (calling_thread != next_thread) {
|
||
linux_port_block_thread(calling_thread);
|
||
}
|
||
}
|
||
|
||
void vPortYieldWithinApi(void)
|
||
{
|
||
vPortYield();
|
||
}
|
||
|
||
void vPortSuspendScheduler(void)
|
||
{
|
||
/* scheduled out task trying to suspend the scheduler should get blocked here */
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
|
||
/* get the metadata of the pthread calling this function */
|
||
thread_t *calling_thread = linux_port_get_calling_thread();
|
||
|
||
/* get the thread metadata from the scheduled task */
|
||
TaskHandle_t scheduled_task_hdl = xTaskGetCurrentTaskHandle();
|
||
|
||
if (linux_port_handle_deleted_task(scheduled_task_hdl, calling_thread)) {
|
||
return;
|
||
}
|
||
|
||
thread_t *scheduled_thread = linux_port_get_thread_from_handle(scheduled_task_hdl);
|
||
if (calling_thread != scheduled_thread) {
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
vPortYield();
|
||
return;
|
||
}
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
}
|
||
|
||
void vPortDisableInterrupts(void)
|
||
{
|
||
vPortEnterCritical();
|
||
}
|
||
|
||
void vPortEnableInterrupts(void)
|
||
{
|
||
vPortExitCritical();
|
||
}
|
||
|
||
BaseType_t xPortSetInterruptMask(void)
|
||
{
|
||
vPortEnterCritical();
|
||
return pdTRUE;
|
||
}
|
||
|
||
void vPortClearInterruptMask(BaseType_t xMask)
|
||
{
|
||
vPortExitCritical();
|
||
}
|
||
|
||
void vPortThreadDying(void *pxTaskToDelete, volatile BaseType_t *pxPendYield)
|
||
{
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
|
||
thread_t *thread = linux_port_get_thread_from_handle((TaskHandle_t)pxTaskToDelete);
|
||
if (thread == NULL) {
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
return;
|
||
}
|
||
|
||
/* Mark the thread as dying, cancel the thread. the pthread
|
||
* will be stopped on next cancellation point. Do not remove the
|
||
* thread item from the list since it will be done in vPortCancelThread */
|
||
thread->is_dying = true;
|
||
pthread_cancel(thread->pthread);
|
||
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
}
|
||
|
||
#if CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS
|
||
static void vPortTLSPointersDelCb(void *pxTCB)
|
||
{
|
||
StaticTask_t *tcb = (StaticTask_t *)pxTCB;
|
||
TlsDeleteCallbackFunction_t *pvDelCbs = (TlsDeleteCallbackFunction_t *)(&tcb->pvDummy15[configNUM_THREAD_LOCAL_STORAGE_POINTERS / 2]);
|
||
|
||
for (int x = 0; x < (configNUM_THREAD_LOCAL_STORAGE_POINTERS / 2); x++) {
|
||
if (pvDelCbs[x] != NULL) {
|
||
pvDelCbs[x](x, tcb->pvDummy15[x]);
|
||
}
|
||
}
|
||
}
|
||
#endif /* CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS */
|
||
|
||
void vPortCancelThread(void *pxTaskToDelete)
|
||
{
|
||
pthread_mutex_lock(&s_port_mutex);
|
||
|
||
#if CONFIG_FREERTOS_TLSP_DELETION_CALLBACKS
|
||
vPortTLSPointersDelCb(pxTaskToDelete);
|
||
#endif
|
||
|
||
thread_t *thread = linux_port_get_thread_from_handle((TaskHandle_t)pxTaskToDelete);
|
||
if (!thread) {
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
return;
|
||
}
|
||
|
||
if (thread->is_dying) {
|
||
/* vPortThreadDying already called */
|
||
} else {
|
||
thread->is_dying = true;
|
||
pthread_cancel(thread->pthread);
|
||
}
|
||
|
||
/* Save fields and unregister while holding the lock. */
|
||
pthread_t pt = thread->pthread;
|
||
event_t *ev = thread->ev;
|
||
linux_port_unregister_thread((TaskHandle_t)pxTaskToDelete);
|
||
|
||
/* Release the mutex before joining – the dying thread may need the
|
||
* scheduler (which also takes s_port_mutex) to reach a cancellation
|
||
* point. */
|
||
pthread_mutex_unlock(&s_port_mutex);
|
||
|
||
pthread_join(pt, NULL);
|
||
event_delete(ev);
|
||
free(thread);
|
||
}
|
||
|
||
void vPortSetStackWatchpoint(void *pxStackStart)
|
||
{
|
||
}
|
||
|
||
#if ( CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS )
|
||
configRUN_TIME_COUNTER_TYPE xPortGetRunTimeCounterValue( void )
|
||
{
|
||
struct timespec ts;
|
||
|
||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||
|
||
return ( configRUN_TIME_COUNTER_TYPE ) ( ( ( uint64_t ) ts.tv_sec * 1000000ULL ) +
|
||
( ( uint64_t ) ts.tv_nsec / 1000ULL ) );
|
||
}
|
||
#endif /* CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS */
|