diff --git a/.github/workflows/firmware.yml b/.github/workflows/firmware.yml index 2656c01a2..76a827089 100644 --- a/.github/workflows/firmware.yml +++ b/.github/workflows/firmware.yml @@ -49,6 +49,10 @@ jobs: - ARDUINO_NANO_RP2040_CONNECT - ARDUINO_NANO_33_BLE_SENSE - DOCKER + profile: [0] # default profile + include: + - target: OPENMV_N6 + profile: 1 fail-fast: false steps: - name: '⏳ Checkout repository' @@ -100,10 +104,10 @@ jobs: run: source tools/ci.sh && ci_install_stedgeai ${HOME}/cache/stedgeai - name: '🏗 Build firmware' - run: source tools/ci.sh && ci_build_target ${{ matrix.target }} + run: source tools/ci.sh && ci_build_target ${{ matrix.target }} ${{ matrix.profile }} - name: '⬆ Upload artifacts' - if: matrix.target != 'DOCKER' + if: matrix.target != 'DOCKER' && matrix.profile == 0 uses: actions/upload-artifact@v4 with: name: ${{ matrix.target }} diff --git a/Makefile b/Makefile index 67c399cb1..f1c44ea26 100755 --- a/Makefile +++ b/Makefile @@ -107,10 +107,24 @@ CFLAGS += -DFB_ALLOC_STATS endif # Enable timing for some functions. -ifeq ($(PROFILE), 1) -CFLAGS += -DOMV_PROFILE_ENABLE=1 +ifeq ($(PROFILE_ENABLE), 1) +$(info ===================================) +$(info ======= Profiling Enabled =======) +$(info ===================================) + +# Enable profiling in IRQ context (default: enabled) +PROFILE_IRQ ?= 0 + +# Default profiler hash table size (must be power of 2) +ifeq ($(PROFILE_HASH),) +PROFILE_HASH=256 endif +CFLAGS += -DOMV_PROFILER_ENABLE=1 +CFLAGS += -DOMV_PROFILER_HASH_SIZE=$(PROFILE_HASH) +CFLAGS += -DOMV_PROFILER_IRQ_ENABLE=$(PROFILE_IRQ) +CFLAGS += -finstrument-functions-exclude-file-list=lib/cmsis,lib/stm32,/lib/mimxrt,lib/alif,simd.h +endif # Include OpenMV board config first to set the port. include $(OMV_BOARD_CONFIG_DIR)/omv_boardconfig.mk diff --git a/common/common.mk b/common/common.mk index c5c9c05ff..93b7562bd 100644 --- a/common/common.mk +++ b/common/common.mk @@ -39,6 +39,7 @@ COMMON_SRC_C += \ usbdbg.c \ vospi.c \ queue.c \ + omv_profiler.c \ CFLAGS += -I$(TOP_DIR)/common OMV_FIRM_OBJ += $(addprefix $(BUILD)/common/, $(COMMON_SRC_C:.c=.o)) diff --git a/common/omv_common.h b/common/omv_common.h index 6707e8639..faecbb7cc 100644 --- a/common/omv_common.h +++ b/common/omv_common.h @@ -31,6 +31,7 @@ #define OMV_ATTR_ALWAYS_INLINE inline __attribute__((always_inline)) #define OMV_ATTR_OPTIMIZE(o) __attribute__((optimize(o))) #define OMV_ATTR_SEC_ALIGN(x, s, a) x __attribute__((section(s), aligned(a))) +#define OMV_ATTR_NO_INSTRUMENT __attribute__((no_instrument_function)) #define OMV_DEBUG_BREAKPOINT() __asm__ volatile ("BKPT") #ifndef __DCACHE_PRESENT @@ -76,17 +77,11 @@ #define OMV_ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) -#if OMV_PROFILE_ENABLE -#include -#include "py/mphal.h" -#define OMV_CONCATENATE_DETAIL(x, y) x##y -#define OMV_CONCATENATE(x, y) OMV_CONCATENATE_DETAIL(x, y) -#define OMV_PROFILE_START(F) mp_uint_t OMV_CONCATENATE(_ticks_start_, F) = mp_hal_ticks_us() -#define OMV_PROFILE_PRINT(F) printf("%s:%s %u us\n", __FUNCTION__, #F, mp_hal_ticks_us() - OMV_CONCATENATE(_ticks_start_, F)) -#else -#define OMV_PROFILE_START(F) -#define OMV_PROFILE_PRINT(F) -#endif +// Token concatenation macros +// OMV_CONCAT expands its arguments before pasting, while +// OMV_CONCAT_HELPER performs the raw token pasting (x##y). +#define OMV_CONCAT_HELPER(x, y) x##y +#define OMV_CONCAT_STR(x, y) OMV_CONCAT_HELPER(x, y) // Returns a pointer to the containing structure // ptr: Pointer to the member within the structure diff --git a/common/omv_profiler.c b/common/omv_profiler.c new file mode 100644 index 000000000..db7607bd5 --- /dev/null +++ b/common/omv_profiler.c @@ -0,0 +1,365 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2025 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV code profiler. + */ +#if OMV_PROFILER_ENABLE +#include +#include +#include + +#include "py/mphal.h" +#include "omv_profiler.h" + +#if __PMU_PRESENT +#define OMV_GET_CYCLE_COUNT() ARM_PMU_Get_CCNTR() +#define OMV_GET_EVENT_COUNT(i) ARM_PMU_Get_EVCNTR(i) +#else +#define OMV_GET_CYCLE_COUNT() 0 +#define OMV_GET_EVENT_COUNT(i) 0 +#endif // __PMU_PRESENT +#define OMV_GET_TICKS_COUNT() ticks_us_monotonic() + +// Call stack entry for tracking nested calls +typedef struct { + void *func_addr; // Function address + void *call_addr; // Call site address + + uint32_t enter_ticks; // Timestamp on entry + uint32_t enter_cycles; // Cycle count on entry + + uint64_t child_ticks; // Total child execution time + uint64_t child_cycles; // Total child cycle count + + #if __PMU_PRESENT + uint16_t enter_events[__PMU_NUM_EVENTCNT]; // PMU events on entry + uint64_t child_events[__PMU_NUM_EVENTCNT]; // Total child PMU events + #endif +} omv_stack_entry_t; + +typedef struct { + bool initialized; // Profiler state. + mutex_t mutex; // Protects profile data + bool reset_pending; // Reset requested flag + omv_profiler_mode_t mode; // Inclusive/exclusive mode + + uint32_t collisions; // Hash table collision count + omv_profiler_data_t *hash[OMV_PROFILER_HASH_SIZE]; // Hash table buckets + + uint32_t pool_index; // Next free pool entry + omv_profiler_data_t pool[OMV_PROFILER_HASH_SIZE]; // Entry pool + + int32_t stack_top; // Current stack position + uint32_t stack_depth; // Max stack depth reached + omv_stack_entry_t stack[OMV_PROFILER_STACK_DEPTH]; // Call stack +} omv_profiler_state_t; + +static omv_profiler_state_t profiler; + +#define profiler_stack_top(x) profiler.stack[profiler.stack_top].x +#define profiler_stack_prv(x) profiler.stack[profiler.stack_top - 1].x + +static OMV_ATTR_NO_INSTRUMENT mp_uint_t ticks_us_monotonic(void) { + static mp_uint_t last_timestamp = 0; + mp_uint_t current = mp_hal_ticks_us(); + + // Ensure monotonic behavior + if (current > last_timestamp) { + last_timestamp = current; + } + + return last_timestamp; +} + +static inline uint32_t OMV_ATTR_NO_INSTRUMENT hash_address(void *addr) { + uint32_t x = (uint32_t) addr >> 2; // drop thumb bit + alignment bits + // mix (Murmur finalizer style) + x ^= x >> 16; + x *= 0x7feb352dU; + x ^= x >> 15; + x *= 0x846ca68bU; + x ^= x >> 16; + return x & (OMV_PROFILER_HASH_SIZE - 1); +} + +// Initialize profiling system +void omv_profiler_init(void) { + if (!profiler.initialized) { + // Reset state + memset(&profiler, 0, sizeof(profiler)); + #if __PMU_PRESENT + // Enable PMU + ARM_PMU_Enable(); + // Disable all event counters + ARM_PMU_CNTR_Disable(((1U << __PMU_NUM_EVENTCNT) - 1)); + // Enable cycles counter. + ARM_PMU_CNTR_Enable(PMU_CNTENSET_CCNTR_ENABLE_Msk); + // Enable Trace + CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; + #endif + } + + // Initialize mutex + mutex_init0(&profiler.mutex); + profiler.initialized = true; +} + +void omv_profiler_reset(void) { + if (!mutex_try_lock(&profiler.mutex, MUTEX_TID_OMV)) { + // Set a pending reset if the data is locked. + profiler.reset_pending = true; + } else { + // Reset state + profiler.pool_index = 0; + profiler.stack_top = -1; + profiler.stack_depth = 0; + profiler.collisions = 0; + profiler.reset_pending = false; + + // Clear hash table + memset(profiler.hash, 0, sizeof(profiler.hash)); + memset(profiler.pool, 0, sizeof(profiler.pool)); + memset(profiler.stack, 0, sizeof(profiler.stack)); + + mutex_unlock(&profiler.mutex, MUTEX_TID_OMV); + } +} + +mutex_t *omv_profiler_lock(void) { + return &profiler.mutex; +} + +void *omv_profiler_get_data(size_t *count) { + *count = profiler.pool_index; + return profiler.pool; +} + +void omv_profiler_set_mode(uint32_t mode) { + profiler.mode = mode; + profiler.reset_pending = true; +} + +void omv_profiler_set_event(uint32_t num, uint32_t type) { + #if __PMU_PRESENT + if (num < __PMU_NUM_EVENTCNT) { + ARM_PMU_Disable(); + + // Configure and enable event counter. + ARM_PMU_Set_EVTYPER(num, type); + ARM_PMU_CNTR_Enable(1 << num); + + // Reset all event counters + ARM_PMU_EVCNTR_ALL_Reset(); + + // Enable PMU + ARM_PMU_Enable(); + } + #endif + profiler.reset_pending = true; +} + +static omv_profiler_data_t *omv_profiler_get_entry(void *func_addr) { + uint32_t hash = hash_address(func_addr); + omv_profiler_data_t *entry = profiler.hash[hash]; + + // Search existing entries in collision chain + while (entry != NULL) { + if (entry->func_addr == func_addr) { + return entry; + } + entry = entry->next; + } + + // Create new entry if not found + if (profiler.pool_index >= OMV_PROFILER_HASH_SIZE) { + // Pool exhausted + return NULL; + } + + entry = &profiler.pool[profiler.pool_index++]; + + memset(entry, 0, sizeof(omv_profiler_data_t)); + entry->min_ticks = UINT32_MAX; + + // Insert at head of collision chain + if (profiler.hash[hash] != NULL) { + profiler.collisions++; + } + + entry->next = profiler.hash[hash]; + profiler.hash[hash] = entry; + + return entry; +} + +void OMV_ATTR_NO_INSTRUMENT __cyg_profile_func_enter(void *func_addr, void *call_addr) { + uint32_t enter_ticks = OMV_GET_TICKS_COUNT(); + uint32_t enter_cycles = OMV_GET_CYCLE_COUNT(); + + if (func_addr == NULL || call_addr == NULL) { + return; + } + + // Skip profiling if called from IRQ context + #if !OMV_PROFILER_IRQ_ENABLE + if ((SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk)) { + return; + } + #endif + + // Restart profiler if needed + if (profiler.reset_pending) { + return omv_profiler_reset(); + } + + // Update call stack + profiler.stack_top++; + if (profiler.stack_top >= OMV_PROFILER_STACK_DEPTH) { + profiler.stack_top = OMV_PROFILER_STACK_DEPTH - 1; + return; + } + + if (profiler.stack_top > (int32_t)profiler.stack_depth) { + profiler.stack_depth = profiler.stack_top; + } + + profiler_stack_top(func_addr) = func_addr; + profiler_stack_top(call_addr) = call_addr; + + profiler_stack_top(child_ticks) = 0; + profiler_stack_top(enter_ticks) = enter_ticks; + + profiler_stack_top(child_cycles) = 0; + profiler_stack_top(enter_cycles) = enter_cycles; + + #if __PMU_PRESENT + for (size_t i = 0; i < __PMU_NUM_EVENTCNT; i++) { + profiler_stack_top(child_events[i]) = 0; + profiler_stack_top(enter_events[i]) = OMV_GET_EVENT_COUNT(i); + } + #endif +} + +void OMV_ATTR_NO_INSTRUMENT __cyg_profile_func_exit(void *func_addr, void *call_addr) { + uint32_t exit_ticks = OMV_GET_TICKS_COUNT(); + uint32_t exit_cycles = OMV_GET_CYCLE_COUNT(); + + if (func_addr == NULL || call_addr == NULL) { + return; + } + + // Skip profiling if called from IRQ context + #if !OMV_PROFILER_IRQ_ENABLE + if ((SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk)) { + return; + } + #endif + + // Restart profiler if needed + if (profiler.reset_pending) { + return omv_profiler_reset(); + } + + // Update events + #if __PMU_PRESENT + uint16_t exit_events[__PMU_NUM_EVENTCNT]; + for (size_t i = 0; i < __PMU_NUM_EVENTCNT; i++) { + exit_events[i] = OMV_GET_EVENT_COUNT(i); + } + #endif + + if (profiler.stack_top < 0 || profiler.stack_top >= OMV_PROFILER_STACK_DEPTH) { + return; + } + + // Function address should match the stack's top. + if (func_addr != profiler_stack_top(func_addr) || + call_addr != profiler_stack_top(call_addr)) { + goto exit_cleanup; + } + + // Protect profile data update via mutex. + if (!mutex_try_lock(&profiler.mutex, MUTEX_TID_OMV)) { + goto exit_cleanup; + } + + // Calculate inclusive ticks and cycles + uint32_t incl_ticks = exit_ticks - profiler_stack_top(enter_ticks); + uint32_t incl_cycles = exit_cycles - profiler_stack_top(enter_cycles); + + // Calculate exclusive ticks and cycles + uint32_t excl_ticks = incl_ticks - profiler_stack_top(child_ticks); + uint32_t excl_cycles = incl_cycles - profiler_stack_top(child_cycles); + + // Check if this entry makes sense. + if (incl_ticks < excl_ticks || incl_cycles < excl_cycles) { + goto mutex_unlock; + } + + // Get or create a new entry + omv_profiler_data_t *entry = omv_profiler_get_entry(func_addr); + if (entry == NULL) { + goto mutex_unlock; + } + + entry->call_count++; + entry->func_addr = func_addr; + entry->call_addr = call_addr; + + entry->total_ticks += excl_ticks; + entry->total_cycles += excl_cycles; + + if (excl_ticks > entry->max_ticks) { + entry->max_ticks = excl_ticks; + } + + if (excl_ticks < entry->min_ticks) { + entry->min_ticks = excl_ticks; + } + + // In exclusive mode, updated the parent's child counters. + if (profiler.stack_top && profiler.mode == OMV_PROFILER_EXCLUSIVE) { + profiler_stack_prv(child_ticks) += incl_ticks; + profiler_stack_prv(child_cycles) += incl_cycles; + } + + #if __PMU_PRESENT + for (size_t i = 0; i < __PMU_NUM_EVENTCNT; i++) { + uint16_t incl_events = exit_events[i] - profiler_stack_top(enter_events[i]); + uint16_t excl_events = incl_events - profiler_stack_top(child_events[i]); + + entry->total_events[i] += excl_events; + + // In exclusive mode, updated the parent's child counters. + if (profiler.stack_top && profiler.mode == OMV_PROFILER_EXCLUSIVE) { + profiler_stack_prv(child_events[i]) += incl_events; + } + } + #endif + +mutex_unlock: + mutex_unlock(&profiler.mutex, MUTEX_TID_OMV); +exit_cleanup: + profiler.stack_top--; +} +#endif // OMV_PROFILER_ENABLE diff --git a/common/omv_profiler.h b/common/omv_profiler.h new file mode 100644 index 000000000..af872fdbe --- /dev/null +++ b/common/omv_profiler.h @@ -0,0 +1,102 @@ +/* + * SPDX-License-Identifier: MIT + * + * Copyright (C) 2025 OpenMV, LLC. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * OpenMV code profiler. + */ +#ifndef __OMV_PROFILER_H__ +#define __OMV_PROFILER_H__ + +#if OMV_PROFILER_ENABLE +#include +#include "py/mphal.h" +#if __PMU_PRESENT +#include "pmu_armv8.h" +#endif +#ifndef __cplusplus +#include "common/mutex.h" +#endif +#include "common/omv_common.h" + +#ifndef __PMU_NUM_EVENTCNT +#define __PMU_NUM_EVENTCNT 0 +#endif + +// Must be a power of 2 +#ifndef OMV_PROFILER_HASH_SIZE +#define OMV_PROFILER_HASH_SIZE 256 +#endif + +#ifndef OMV_PROFILER_STACK_DEPTH +#define OMV_PROFILER_STACK_DEPTH 32 +#endif + +typedef enum { + OMV_PROFILER_INCLUSIVE = 0, + OMV_PROFILER_EXCLUSIVE = 1, +} omv_profiler_mode_t; + +// Profile record structure +typedef struct __attribute__((packed)) _prof_data { + void *func_addr; // Function address + void *call_addr; // Caller address + uint32_t call_count; // Number of times called + uint32_t min_ticks; // Minimum execution time + uint32_t max_ticks; // Maximum execution time + uint64_t total_ticks; // Total time in ticks + uint64_t total_cycles; // Total CPU cycles + #if __PMU_PRESENT + uint64_t total_events[__PMU_NUM_EVENTCNT]; + #endif + struct _prof_data *next; // Next in hash collision chain +} omv_profiler_data_t; + +OMV_ATTR_NO_INSTRUMENT void omv_profiler_init(void); +OMV_ATTR_NO_INSTRUMENT void omv_profiler_reset(void); +OMV_ATTR_NO_INSTRUMENT void *omv_profiler_get_data(size_t *count); +OMV_ATTR_NO_INSTRUMENT void omv_profiler_set_mode(uint32_t mode); +OMV_ATTR_NO_INSTRUMENT void omv_profiler_set_event(uint32_t num, uint32_t type); +#ifndef __cplusplus +OMV_ATTR_NO_INSTRUMENT mutex_t *omv_profiler_lock(void); +#endif + +// Manual instrumentation macros +#define OMV_PROFILER_ENTER(func) \ + do { \ + void *func_addr = (void*)(func); \ + void *call_addr = __builtin_return_address(0); \ + __cyg_profile_func_enter(func_addr, call_addr); \ + } while(0) + +#define OMV_PROFILER_EXIT(func) \ + do { \ + void *func_addr = (void*)(func); \ + void *call_addr = __builtin_return_address(0); \ + __cyg_profile_func_exit(func_addr, call_addr); \ + } while(0) + +#else +// Disabled - empty macros +#define OMV_PROFILER_ENTER(func) do {} while(0) +#define OMV_PROFILER_EXIT(func) do {} while(0) +#endif // OMV_PROFILER_ENABLE +#endif // __OMV_PROFILER_H__ diff --git a/common/usbdbg.c b/common/usbdbg.c index 05dd303a2..276691139 100644 --- a/common/usbdbg.c +++ b/common/usbdbg.c @@ -57,7 +57,6 @@ extern uint32_t usb_cdc_buf_len(); extern uint32_t usb_cdc_get_buf(uint8_t *buf, uint32_t len); extern void usb_cdc_reset_buffers(void); - void usbdbg_init() { cmd = USBDBG_NONE; script_ready = false; @@ -67,6 +66,9 @@ void usbdbg_init() { #if OMV_TUSBDBG_ENABLE tinyusb_debug_init(); #endif + #if OMV_PROFILER_ENABLE + omv_profiler_init(); + #endif } bool usbdbg_script_ready() { @@ -166,7 +168,7 @@ void usbdbg_data_in(uint32_t size, usbdbg_write_callback_t write_callback) { // Return 0 if FB is locked or not ready. uint32_t buffer[3] = { 0 }; // Try to lock FB. If header size == 0 frame is not ready - if (mutex_try_lock_fair(&JPEG_FB()->lock, MUTEX_TID_IDE)) { + if (mutex_try_lock(&JPEG_FB()->lock, MUTEX_TID_IDE)) { // If header size == 0 frame is not ready if (JPEG_FB()->size == 0) { // unlock FB @@ -231,15 +233,23 @@ void usbdbg_data_in(uint32_t size, usbdbg_write_callback_t write_callback) { // Set script running flag if (script_running) { - buffer[0] |= USBDBG_STATE_FLAGS_SCRIPT; + buffer[0] |= USBDBG_FLAG_SCRIPT_RUNNING; } // Set text buf valid flag. uint32_t tx_buf_len = usb_cdc_buf_len(); if (tx_buf_len) { - buffer[0] |= USBDBG_STATE_FLAGS_TEXT; + buffer[0] |= USBDBG_FLAG_TEXTBUF_NOTEMPTY; } + // Set code profiling flags + #if OMV_PROFILER_ENABLE + buffer[0] |= USBDBG_FLAG_PROFILE_ENABLED; + #if __PMU_PRESENT + buffer[0] |= USBDBG_FLAG_PROFILE_HAS_PMU; + #endif // __PMU_PRESENT + #endif // OMV_PROFILER_ENABLE + // Limit the frames sent over USB to 20Hz. if (check_timeout_ms(last_update_ms, 50) && mutex_try_lock_fair(&JPEG_FB()->lock, MUTEX_TID_IDE)) { @@ -249,7 +259,7 @@ void usbdbg_data_in(uint32_t size, usbdbg_write_callback_t write_callback) { mutex_unlock(&JPEG_FB()->lock, MUTEX_TID_IDE); } else { // Set valid frame flag. - buffer[0] |= USBDBG_STATE_FLAGS_FRAME; + buffer[0] |= USBDBG_FLAG_FRAMEBUF_LOCKED; // Set frame width, height and size/bpp buffer[1] = JPEG_FB()->w; @@ -272,6 +282,40 @@ void usbdbg_data_in(uint32_t size, usbdbg_write_callback_t write_callback) { break; } + case USBDBG_PROFILE_SIZE: { + // Return 0 if the profiling data is locked. + uint32_t buffer[3] = { 0 }; + #if OMV_PROFILER_ENABLE + if (mutex_try_lock(omv_profiler_lock(), MUTEX_TID_IDE)) { + size_t count = 0; + (void) omv_profiler_get_data(&count); + + buffer[0] = count; + buffer[1] = sizeof(omv_profiler_data_t); + buffer[2] = __PMU_NUM_EVENTCNT; + } + #endif + cmd = USBDBG_NONE; + write_callback(&buffer, sizeof(buffer)); + break; + } + + #if OMV_PROFILER_ENABLE + case USBDBG_PROFILE_DUMP: + if (xfer_offs < xfer_size) { + size_t count = 0; + char *data = omv_profiler_get_data(&count); + + write_callback(data + xfer_offs, size); + xfer_offs += size; + if (xfer_offs == xfer_size) { + cmd = USBDBG_NONE; + mutex_unlock(omv_profiler_lock(), MUTEX_TID_IDE); + } + } + break; + #endif + default: /* error */ break; } @@ -310,6 +354,26 @@ void usbdbg_data_out(uint32_t size, usbdbg_read_callback_t read_callback) { } break; + case USBDBG_PROFILE_MODE: { + uint32_t buffer[1]; + read_callback(&buffer, sizeof(buffer)); + #if OMV_PROFILER_ENABLE + omv_profiler_set_mode(buffer[0]); + #endif + cmd = USBDBG_NONE; + break; + } + + case USBDBG_PROFILE_EVENT: { + uint32_t buffer[2]; + read_callback(&buffer, sizeof(buffer)); + #if OMV_PROFILER_ENABLE + omv_profiler_set_event(buffer[0], buffer[1]); + #endif + cmd = USBDBG_NONE; + break; + } + default: /* error */ break; } @@ -327,17 +391,29 @@ void usbdbg_control(void *buffer, uint8_t request, uint32_t size) { case USBDBG_TX_BUF_LEN: case USBDBG_SENSOR_ID: case USBDBG_GET_STATE: - xfer_offs = 0; - xfer_size = size; - break; - - case USBDBG_SCRIPT_RUNNING: - vstr_reset(&script_buf); + case USBDBG_PROFILE_SIZE: + case USBDBG_PROFILE_DUMP: + case USBDBG_PROFILE_MODE: + case USBDBG_PROFILE_EVENT: case USBDBG_SCRIPT_EXEC: xfer_offs = 0; xfer_size = size; break; + case USBDBG_PROFILE_RESET: { + #if OMV_PROFILER_ENABLE + omv_profiler_reset(); + #endif + cmd = USBDBG_NONE; + break; + } + + case USBDBG_SCRIPT_RUNNING: + vstr_reset(&script_buf); + xfer_offs = 0; + xfer_size = size; + break; + case USBDBG_SCRIPT_STOP: if (script_running) { // Reset CDC buffers. diff --git a/common/usbdbg.h b/common/usbdbg.h index 6d4fd325a..4a0895ffa 100644 --- a/common/usbdbg.h +++ b/common/usbdbg.h @@ -61,13 +61,20 @@ enum usbdbg_cmd { USBDBG_TX_BUF = 0x8F, USBDBG_SENSOR_ID = 0x90, USBDBG_GET_STATE = 0x93, + USBDBG_PROFILE_SIZE = 0x94, + USBDBG_PROFILE_DUMP = 0x95, + USBDBG_PROFILE_MODE = 0x16, + USBDBG_PROFILE_EVENT = 0x17, + USBDBG_PROFILE_RESET = 0x18, }; -enum usbdbg_state_flags { - USBDBG_STATE_FLAGS_SCRIPT = (1 << 0), - USBDBG_STATE_FLAGS_TEXT = (1 << 1), - USBDBG_STATE_FLAGS_FRAME = (1 << 2), -}; +typedef enum { + USBDBG_FLAG_SCRIPT_RUNNING = (1 << 0), + USBDBG_FLAG_TEXTBUF_NOTEMPTY = (1 << 1), + USBDBG_FLAG_FRAMEBUF_LOCKED = (1 << 2), + USBDBG_FLAG_PROFILE_ENABLED = (1 << 3), + USBDBG_FLAG_PROFILE_HAS_PMU = (1 << 4), +} usbdbg_flags_t; typedef uint32_t (*usbdbg_read_callback_t) (void *buf, uint32_t len); typedef uint32_t (*usbdbg_write_callback_t) (const void *buf, uint32_t len); diff --git a/lib/imlib/bayer.c b/lib/imlib/bayer.c index 696cd47ef..7c45f6cf9 100644 --- a/lib/imlib/bayer.c +++ b/lib/imlib/bayer.c @@ -1160,7 +1160,6 @@ void imlib_debayer_line(int x_start, int x_end, int y_row, void *dst_row_ptr, pi // assumes dst->h == src->h // src and dst may not overlap, but, faster than imlib_debayer_image_awb void imlib_debayer_image(image_t *dst, image_t *src) { - OMV_PROFILE_START(); rectangle_t roi = { .x = 0, .y = 0, @@ -1168,7 +1167,6 @@ void imlib_debayer_image(image_t *dst, image_t *src) { .h = src->h, }; vdebayer(src, &roi, 0, dst); - OMV_PROFILE_PRINT(); } #if defined(IMLIB_ENABLE_DEBAYER_OPTIMIZATION) @@ -2294,8 +2292,6 @@ static void vdebayer_rggb_to_rgb565_awb_quarter(image_t *src, image_t *dst, uint // RGB565: src->data == dst->data + image_size(src) // YUV422: Not supported void imlib_debayer_image_awb(image_t *dst, image_t *src, bool fast, uint32_t r_out, uint32_t g_out, uint32_t b_out) { - OMV_PROFILE_START(); - uint32_t red_gain = IM_DIV(g_out * 32, r_out); red_gain = IM_MIN(red_gain, 128U); @@ -2459,6 +2455,4 @@ void imlib_debayer_image_awb(image_t *dst, image_t *src, bool fast, uint32_t r_o } } } - - OMV_PROFILE_PRINT(); } diff --git a/lib/imlib/draw.c b/lib/imlib/draw.c index cc15b0458..5753a8cb0 100644 --- a/lib/imlib/draw.c +++ b/lib/imlib/draw.c @@ -2838,7 +2838,6 @@ void imlib_draw_image(image_t *dst_img, imlib_draw_row_callback_t callback, void *callback_arg, void *dst_row_override) { - OMV_PROFILE_START(); int dst_delta_x = 1; // positive direction if (x_scale < 0.f) { // flip X @@ -5404,7 +5403,6 @@ exit_cleanup: if (&new_src_img == src_img) { fb_free(); } - OMV_PROFILE_PRINT(); } #ifdef IMLIB_ENABLE_FLOOD_FILL diff --git a/lib/imlib/imlib.h b/lib/imlib/imlib.h index 4da9840a6..bf5e78219 100644 --- a/lib/imlib/imlib.h +++ b/lib/imlib/imlib.h @@ -44,6 +44,7 @@ #include "imlib_config.h" #include "omv_boardconfig.h" #include "omv_common.h" +#include "omv_profiler.h" #include "py/runtime.h" // Enables 38 TensorFlow Lite operators. diff --git a/lib/imlib/imlib.mk b/lib/imlib/imlib.mk index f5824bdd6..f55bb4760 100644 --- a/lib/imlib/imlib.mk +++ b/lib/imlib/imlib.mk @@ -85,4 +85,11 @@ ifeq ($(CLANG_ENABLE),1) OMV_CLANG_OBJ = $(BUILD)/lib/imlib/bayer.o endif +# Enable instrumentation. +ifeq ($(PROFILE_ENABLE), 1) +$(BUILD)/lib/imlib/%.o: override CFLAGS += -finstrument-functions +# Clang does not support -finstrument-functions-exclude-file-list. +$(OMV_CLANG_OBJ): override CFLAGS := $(filter-out -finstrument-functions-exclude-file-list=%,$(CFLAGS)) +endif + OMV_FIRM_OBJ += $(addprefix $(BUILD)/lib/imlib/, $(IMLIB_SRC_C:.c=.o)) diff --git a/lib/imlib/jpegd.c b/lib/imlib/jpegd.c index 06298c45c..e130b2fa1 100644 --- a/lib/imlib/jpegd.c +++ b/lib/imlib/jpegd.c @@ -2858,7 +2858,6 @@ static int DecodeJPEG(JPEGIMAGE *pJPEG) { } void jpeg_decompress(image_t *dst, image_t *src) { - OMV_PROFILE_START(); JPEGIMAGE jpg; // Supports decoding baseline JPEGs only. @@ -2898,7 +2897,5 @@ void jpeg_decompress(image_t *dst, image_t *src) { if (JPEG_decode(&jpg, 0, 0, 0) == 0) { mp_raise_msg(&mp_type_OSError, MP_ERROR_TEXT("JPEG decoder failed.")); } - - OMV_PROFILE_PRINT(); } #endif diff --git a/lib/imlib/jpege.c b/lib/imlib/jpege.c index 03f4f7a42..fb1b9cd86 100644 --- a/lib/imlib/jpege.c +++ b/lib/imlib/jpege.c @@ -931,8 +931,6 @@ static void jpeg_write_headers(jpeg_buf_t *jpeg_buf, int w, int h, int bpp, jpeg } bool jpeg_compress(image_t *src, image_t *dst, int quality, bool realloc, jpeg_subsampling_t subsampling) { - OMV_PROFILE_START(); - if (!dst->data) { uint32_t size = 0; dst->data = fb_alloc_all(&size, FB_ALLOC_PREFER_SIZE | FB_ALLOC_CACHE_ALIGN); @@ -1261,8 +1259,6 @@ bool jpeg_compress(image_t *src, image_t *dst, int quality, bool realloc, jpeg_s dst->size = jpeg_buf.idx; dst->data = jpeg_buf.buf; - - OMV_PROFILE_PRINT(); return false; } diff --git a/lib/imlib/png.c b/lib/imlib/png.c index b20c04212..4450d66d5 100644 --- a/lib/imlib/png.c +++ b/lib/imlib/png.c @@ -120,8 +120,6 @@ unsigned lodepng_convert_cb(unsigned char *out, const unsigned char *in, #if defined(IMLIB_ENABLE_PNG_ENCODER) bool png_compress(image_t *src, image_t *dst) { - OMV_PROFILE_START(); - if (src->is_compressed) { return true; } @@ -192,14 +190,12 @@ bool png_compress(image_t *src, image_t *dst) { // free fb_alloc() memory used for umm_init_x(). fb_free(); // umm_init_x(); } - OMV_PROFILE_PRINT(); return false; } #endif // IMLIB_ENABLE_PNG_ENCODER #if defined(IMLIB_ENABLE_PNG_DECODER) void png_decompress(image_t *dst, image_t *src) { - OMV_PROFILE_START(); umm_init_x(fb_avail()); LodePNGState state; @@ -241,7 +237,6 @@ void png_decompress(image_t *dst, image_t *src) { // free fb_alloc() memory used for umm_init_x(). fb_free(); // umm_init_x(); - OMV_PROFILE_PRINT(); } #endif // IMLIB_ENABLE_PNG_DECODER #endif // IMLIB_ENABLE_PNG_ENCODER || IMLIB_ENABLE_PNG_DECODER diff --git a/lib/stai/stai.mk b/lib/stai/stai.mk index 46db868a8..e1298172b 100644 --- a/lib/stai/stai.mk +++ b/lib/stai/stai.mk @@ -78,6 +78,11 @@ $(BUILD)/lib/stai/libstai/ll_aton/%.o: override CFLAGS += \ -Wno-double-promotion \ $(STAI_CFLAGS) \ +# Enable instrumentation. +ifeq ($(PROFILE_ENABLE), 1) +$(BUILD)/lib/stai/stai_backend.o: override CFLAGS += -finstrument-functions +endif + OMV_CFLAGS += -I$(TOP_DIR)/lib/stai/libstai/include OMV_FIRM_OBJ += $(addprefix $(BUILD)/lib/stai/, $(STAI_SRC_C:.c=.o)) endif diff --git a/lib/tflm/tflm.mk b/lib/tflm/tflm.mk index 1ff698e96..de40e8ca4 100644 --- a/lib/tflm/tflm.mk +++ b/lib/tflm/tflm.mk @@ -50,6 +50,5 @@ $(BUILD)/lib/tflm/tflm_backend.o: CXXFLAGS = \ OMV_CFLAGS += -I$(TOP_DIR)/lib/tflm/libtflm/include OMV_CFLAGS += -I$(TOP_DIR)/lib/tflm/libtflm/include/third_party/ethos_u_core_driver/include - OMV_FIRM_OBJ += $(addprefix $(BUILD)/lib/tflm/, $(TFLM_SRC_CC:.cc=.o)) endif diff --git a/lib/tflm/tflm_backend.cc b/lib/tflm/tflm_backend.cc index 078ea4b51..7a134b084 100644 --- a/lib/tflm/tflm_backend.cc +++ b/lib/tflm/tflm_backend.cc @@ -51,6 +51,7 @@ extern "C" { #include "py/binary.h" #include "py/gc.h" #include "py_ml.h" +#include "common/omv_profiler.h" using namespace tflite; #define TF_ARENA_EXTRA (512) @@ -319,6 +320,8 @@ int ml_backend_init_model(py_ml_model_obj_t *model) { } int ml_backend_run_inference(py_ml_model_obj_t *model) { + OMV_PROFILER_ENTER(ml_backend_run_inference); + RegisterDebugLogCallback(ml_backend_log_handler); ml_backend_state_t *state = (ml_backend_state_t *) model->state; @@ -326,6 +329,7 @@ int ml_backend_run_inference(py_ml_model_obj_t *model) { mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("Invoke failed")); } + OMV_PROFILER_EXIT(ml_backend_run_inference); return 0; } diff --git a/modules/micropython.mk b/modules/micropython.mk index 57549f09e..728f89178 100644 --- a/modules/micropython.mk +++ b/modules/micropython.mk @@ -66,3 +66,10 @@ ifeq ($(DEBUG), 0) # Use a higher optimization level for user C modules. $(BUILD)/modules/%.o: override CFLAGS += $(USERMOD_OPT) endif + +ifeq ($(PROFILE_ENABLE), 1) +$(BUILD)/modules/py_ml.o: override CFLAGS += -finstrument-functions +$(BUILD)/modules/py_image.o: override CFLAGS += -finstrument-functions +$(BUILD)/modules/ulab/%.o: override CFLAGS += -finstrument-functions +endif + diff --git a/modules/py_image.c b/modules/py_image.c index 244d61690..5dc3fc523 100644 --- a/modules/py_image.c +++ b/modules/py_image.c @@ -731,7 +731,6 @@ static MP_DEFINE_CONST_FUN_OBJ_1(py_image_bytearray_obj, py_image_bytearray); #if defined(MODULE_ULAB_ENABLED) && (ULAB_MAX_DIMS == 4) static mp_obj_t py_image_to_ndarray(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { - OMV_PROFILE_START(); enum { ARG_dtype, ARG_buffer }; static const mp_arg_t allowed_args[] = { { MP_QSTR_dtype, MP_ARG_OBJ | MP_ARG_REQUIRED, {.u_rom_obj = MP_ROM_NONE } }, @@ -906,8 +905,6 @@ static mp_obj_t py_image_to_ndarray(size_t n_args, const mp_obj_t *pos_args, mp_ } } } - - OMV_PROFILE_PRINT(); return MP_OBJ_FROM_PTR(ndarray); } static MP_DEFINE_CONST_FUN_OBJ_KW(py_image_to_ndarray_obj, 1, py_image_to_ndarray); diff --git a/modules/py_ml.c b/modules/py_ml.c index 9243492ef..a53ca9ac0 100644 --- a/modules/py_ml.c +++ b/modules/py_ml.c @@ -254,14 +254,8 @@ static mp_obj_t py_ml_model_predict(size_t n_args, const mp_obj_t *pos_args, mp_ mp_raise_msg(&mp_type_ValueError, MP_ERROR_TEXT("Unsupported input type. Expected a list")); } - OMV_PROFILE_START(preprocess); py_ml_process_input(model, pos_args[1]); - OMV_PROFILE_PRINT(preprocess); - - OMV_PROFILE_START(inference); ml_backend_run_inference(model); - OMV_PROFILE_PRINT(inference); - mp_obj_t output = py_ml_process_output(model); if (args[ARG_callback].u_obj != mp_const_none) { diff --git a/ports/alif/omv_gpu.c b/ports/alif/omv_gpu.c index 95abfad8b..9cc2775b7 100644 --- a/ports/alif/omv_gpu.c +++ b/ports/alif/omv_gpu.c @@ -135,7 +135,6 @@ int omv_gpu_draw_image(image_t *src_img, if (color_palette || alpha_palette || transform) { return -1; } - OMV_PROFILE_START(); d2_s32 err; d2_u32 blit_flags = 0; @@ -183,7 +182,6 @@ int omv_gpu_draw_image(image_t *src_img, // Invalidate the framebuffer image. SCB_InvalidateDCache_by_Addr(dst_img->data, image_size(dst_img)); - OMV_PROFILE_PRINT(); return 0; } diff --git a/ports/ports.mk b/ports/ports.mk index d41a2615a..68cfcd6c6 100644 --- a/ports/ports.mk +++ b/ports/ports.mk @@ -27,3 +27,9 @@ CFLAGS += -I$(TOP_DIR)/ports/$(PORT)/modules PORT_SRC_C = $(wildcard ports/$(PORT)/*.c) OMV_FIRM_OBJ += $(addprefix $(BUILD)/, $(PORT_SRC_C:.c=.o)) + +# Enable instrumentation. +ifeq ($(PROFILE_ENABLE), 1) +$(BUILD)/ports/alif/%.o: override CFLAGS += -finstrument-functions +$(BUILD)/ports/stm32/%.o: override CFLAGS += -finstrument-functions +endif diff --git a/ports/stm32/omv_gpu.c b/ports/stm32/omv_gpu.c index bd977961f..1c0fa1006 100644 --- a/ports/stm32/omv_gpu.c +++ b/ports/stm32/omv_gpu.c @@ -95,8 +95,6 @@ int omv_gpu_draw_image(image_t *src_img, const uint8_t *alpha_palette, image_hint_t hint, float *transform) { - OMV_PROFILE_START(); - // GPU2D can only draw on RGB565/GRAYSCALE buffers. if ((dst_img->pixfmt != PIXFORMAT_RGB565) && (dst_img->pixfmt != PIXFORMAT_GRAYSCALE)) { return -1; @@ -197,8 +195,6 @@ int omv_gpu_draw_image(image_t *src_img, HAL_ICACHE_Invalidate_IT(); SCB_InvalidateDCache_by_Addr(dst_img->data, image_size(dst_img)); - - OMV_PROFILE_PRINT(); return 0; } #else @@ -211,8 +207,6 @@ int omv_gpu_draw_image(image_t *src_img, const uint8_t *alpha_palette, image_hint_t hint, float *transform) { - OMV_PROFILE_START(); - // DMA2D can only draw on RGB565 buffers and the destination/source buffers must be accessible by DMA. if ((dst_img->pixfmt != PIXFORMAT_RGB565) || (!DMA_BUFFER(dst_img->data)) || (!DMA_BUFFER(src_img->data))) { return -1; @@ -401,8 +395,6 @@ int omv_gpu_draw_image(image_t *src_img, #endif HAL_DMA2D_DeInit(&dma2d); - - OMV_PROFILE_PRINT(); return 0; } #endif // OMV_GPU_NEMA diff --git a/ports/stm32/stm_jpeg.c b/ports/stm32/stm_jpeg.c index ff675518a..2d7912572 100644 --- a/ports/stm32/stm_jpeg.c +++ b/ports/stm32/stm_jpeg.c @@ -129,7 +129,6 @@ static void jpeg_compress_data_ready(JPEG_HandleTypeDef *hjpeg, uint8_t *pDataOu } bool jpeg_compress(image_t *src, image_t *dst, int quality, bool realloc, jpeg_subsampling_t subsampling) { - OMV_PROFILE_START(); HAL_JPEG_RegisterGetDataCallback(&JPEG_state.jpeg_descr, jpeg_compress_get_data); HAL_JPEG_RegisterDataReadyCallback(&JPEG_state.jpeg_descr, jpeg_compress_data_ready); @@ -394,8 +393,6 @@ exit_cleanup: HAL_JPEG_UnRegisterGetDataCallback(&JPEG_state.jpeg_descr); fb_free(); // mcu_row_buffer (after DMA is aborted) - - OMV_PROFILE_PRINT(); return jpeg_overflow; } @@ -427,8 +424,6 @@ static void jpeg_decompress_data_ready(JPEG_HandleTypeDef *hjpeg, uint8_t *pData } void jpeg_decompress(image_t *dst, image_t *src) { - OMV_PROFILE_START(); - // Verify the jpeg image is not a non-baseline jpeg image and check that is has // valid headers up to the start-of-scan header (which cannot be trivially walked). if (!jpeg_is_valid(src)) { @@ -795,8 +790,6 @@ exit_cleanup: if (((uint32_t) src->data) % __SCB_DCACHE_LINE_SIZE) { fb_free(); // JPEG_state.jpeg_descr.pJpegInBuffPtr (after DMA is aborted) } - - OMV_PROFILE_PRINT(); } void imlib_hardware_jpeg_init() { diff --git a/tools/ci.sh b/tools/ci.sh index a49a7af7a..c6ef04988 100755 --- a/tools/ci.sh +++ b/tools/ci.sh @@ -54,8 +54,11 @@ ci_build_target() { make -j$(nproc) -C docker TARGET=${BOARD} else make -j$(nproc) -C lib/micropython/mpy-cross - make -j$(nproc) TARGET=${1} - mv build/bin ${1} + make -j$(nproc) TARGET=${1} PROFILE_ENABLE=${2} + # Don't copy artifacts for profiling builds + if [ "$2" = 0 ]; then + mv build/bin ${1} + fi fi } diff --git a/tools/pyopenmv.py b/tools/pyopenmv.py index 35e23db41..328aacc3d 100755 --- a/tools/pyopenmv.py +++ b/tools/pyopenmv.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python # This file is part of the OpenMV project. # # Copyright (c) 2013-2021 Ibrahim Abdelkader @@ -16,7 +16,7 @@ import numpy as np from PIL import Image __serial = None -__FB_HDR_SIZE =12 +__FB_HDR_SIZE = 12 # USB Debug commands __USBDBG_CMD = 48 @@ -38,20 +38,27 @@ __USBDBG_FB_ENABLE = 0x0D __USBDBG_TX_BUF_LEN = 0x8E __USBDBG_TX_BUF = 0x8F __USBDBG_GET_STATE = 0x93 +__USBDBG_PROFILE_SIZE = 0x94 +__USBDBG_PROFILE_DUMP = 0x95 +__USBDBG_PROFILE_MODE = 0x16 +__USBDBG_PROFILE_EVENT = 0x17 +__USBDBG_PROFILE_RESET = 0x18 -__USBDBG_STATE_FLAGS_SCRIPT = (1 << 0) -__USBDBG_STATE_FLAGS_TEXT = (1 << 1) -__USBDBG_STATE_FLAGS_FRAME = (1 << 2) +__USBDBG_FLAG_SCRIPT_RUNNING = (1 << 0) +__USBDBG_FLAG_TEXTBUF_NOTEMPTY = (1 << 1) +__USBDBG_FLAG_FRAMEBUF_LOCKED = (1 << 2) +__USBDBG_FLAG_PROFILE_ENABLED = (1 << 3) +__USBDBG_FLAG_PROFILE_HAS_PMU = (1 << 4) -ATTR_CONTRAST =0 -ATTR_BRIGHTNESS =1 -ATTR_SATURATION =2 -ATTR_GAINCEILING=3 +ATTR_CONTRAST = 0 +ATTR_BRIGHTNESS = 1 +ATTR_SATURATION = 2 +ATTR_GAINCEILING = 3 -__BOOTLDR_START = 0xABCD0001 -__BOOTLDR_RESET = 0xABCD0002 -__BOOTLDR_ERASE = 0xABCD0004 -__BOOTLDR_WRITE = 0xABCD0008 +__BOOTLDR_START = 0xABCD0001 +__BOOTLDR_RESET = 0xABCD0002 +__BOOTLDR_ERASE = 0xABCD0004 +__BOOTLDR_WRITE = 0xABCD0008 def init(port, baudrate=921600, timeout=0.3): global __serial @@ -67,29 +74,40 @@ def disconnect(): except: pass +def write_pack(fmt, *values): + __serial.write(struct.pack(fmt, *values)) + +def read_unpack(fmt): + return struct.unpack(fmt, __serial.read(struct.calcsize(fmt))) + def set_timeout(timeout): __serial.timeout = timeout def fb_size(): # read fb header - __serial.write(struct.pack(" 2 else (w * h * size) # read fb data - __serial.write(struct.pack(" 0 else [] + }) + + offset += record_size + + return records + +def set_profile_mode(mode): + write_pack(" -# Copyright (c) 2013-2021 Kwabena W. Agyeman +# Copyright (c) 2013-2025 Ibrahim Abdelkader +# Copyright (c) 2013-2025 Kwabena W. Agyeman # # This work is licensed under the MIT license, see the file LICENSE for details. # @@ -39,7 +39,355 @@ while(True): img.flush() """ -def pygame_test(port, poll_rate, scale, benchmark): +def addr_to_symbol(symbols, address): + # Binary search for speed + lo, hi = 0, len(symbols) - 1 + while lo <= hi: + mid = (lo + hi) // 2 + start, end, name = symbols[mid] + if start <= address < end: + return name + elif address < start: + hi = mid - 1 + else: + lo = mid + 1 + return None + +def get_color_by_percentage(percentage, base_color=(220, 220, 220)): + """ + Return a color based on percentage with fine-grained intensity levels. + """ + def clamp(value): + return max(0, min(255, int(value))) + + if percentage >= 50: + # Very high - bright red + intensity = min(1.0, (percentage - 50) / 50) + return (255, clamp(120 - 120 * intensity), clamp(120 - 120 * intensity)) + elif percentage >= 30: + # High - red-orange + intensity = (percentage - 30) / 20 + return (255, clamp(160 + 40 * intensity), clamp(160 - 40 * intensity)) + elif percentage >= 20: + # Medium-high - orange + intensity = (percentage - 20) / 10 + return (255, clamp(200 + 55 * intensity), clamp(180 - 20 * intensity)) + elif percentage >= 15: + # Medium - yellow-orange + intensity = (percentage - 15) / 5 + return (255, clamp(220 + 35 * intensity), clamp(180 + 20 * intensity)) + elif percentage >= 10: + # Medium-low - yellow + intensity = (percentage - 10) / 5 + return (clamp(255 - 75 * intensity), 255, clamp(180 + 75 * intensity)) + elif percentage >= 5: + # Low - light green + intensity = (percentage - 5) / 5 + return (clamp(180 + 75 * intensity), 255, clamp(180 + 75 * intensity)) + elif percentage >= 2: + # Very low - green + intensity = (percentage - 2) / 3 + return (clamp(160 + 95 * intensity), clamp(255 - 55 * intensity), clamp(160 + 95 * intensity)) + elif percentage >= 1: + # Minimal - light blue-green + intensity = (percentage - 1) / 1 + return (clamp(140 + 120 * intensity), clamp(200 + 55 * intensity), clamp(255 - 95 * intensity)) + else: + # Zero or negligible - base color + return base_color + +def draw_rounded_rect(surface, color, rect, radius=5): + x, y, w, h = rect + if w <= 0 or h <= 0: + return + pygame.draw.rect(surface, color, (x + radius, y, w - 2*radius, h)) + pygame.draw.rect(surface, color, (x, y + radius, w, h - 2*radius)) + pygame.draw.circle(surface, color, (x + radius, y + radius), radius) + pygame.draw.circle(surface, color, (x + w - radius, y + radius), radius) + pygame.draw.circle(surface, color, (x + radius, y + h - radius), radius) + pygame.draw.circle(surface, color, (x + w - radius, y + h - radius), radius) + + +def draw_table(overlay_surface, config, title, headers, col_widths): + """Draw the common table background, title, and header.""" + # Draw main table background + table_rect = (0, 0, config['width'], config['height']) + draw_rounded_rect(overlay_surface, config['colors']['bg'], table_rect, int(8 * config['scale_factor'])) + pygame.draw.rect(overlay_surface, config['colors']['border'], table_rect, max(1, int(2 * config['scale_factor']))) + + # Table title + title_text = config['fonts']['title'].render(title, True, config['colors']['header_text']) + title_rect = title_text.get_rect() + title_x = (config['width'] - title_rect.width) // 2 + overlay_surface.blit(title_text, (title_x, int(12 * config['scale_factor']))) + + # Header + header_y = int(50 * config['scale_factor']) + header_height = int(40 * config['scale_factor']) + + # Draw header background + header_rect = (int(5 * config['scale_factor']), header_y, + config['width'] - int(10 * config['scale_factor']), header_height) + draw_rounded_rect(overlay_surface, config['colors']['header_bg'], header_rect, int(4 * config['scale_factor'])) + + # Draw header text and separators + current_x = int(10 * config['scale_factor']) + for i, (header, width) in enumerate(zip(headers, col_widths)): + header_surface = config['fonts']['header'].render(header, True, config['colors']['header_text']) + overlay_surface.blit(header_surface, (current_x, header_y + int(6 * config['scale_factor']))) + + if i < len(headers) - 1: + sep_x = current_x + width - int(5 * config['scale_factor']) + pygame.draw.line(overlay_surface, config['colors']['border'], + (sep_x, header_y + 2), (sep_x, header_y + header_height - 2), 1) + current_x += width + + +def draw_event_table(overlay_surface, config, profile_data, profile_mode, symbols): + """Draw the event counter mode table.""" + + # Prepare data + num_events = len(profile_data[0]['events']) if profile_data else 0 + if not num_events: + sorted_data = sorted(profile_data, key=lambda x: x['address']) + else: + sort_func = lambda x: x['events'][0] // max(1, x['call_count']) + sorted_data = sorted(profile_data, key=sort_func, reverse=True) + + headers = ["Function"] + [f"E{i}" for i in range(num_events)] + proportions = [0.30] + [0.70/num_events] * num_events + col_widths = [config['width'] * prop for prop in proportions] + profile_mode = "Exclusive" if profile_mode else "Inclusive" + + # Calculate event totals for percentage calculation + event_totals = [0] * num_events + for record in sorted_data: + for i, event_count in enumerate(record['events']): + event_totals[i] += event_count // max(1, record['call_count']) + + # Draw table structure + draw_table(overlay_surface, config, f"Event Counters ({profile_mode})", headers, col_widths) + + # Draw data rows + row_height = int(30 * config['scale_factor']) + data_start_y = int(50 * config['scale_factor'] + 40 * config['scale_factor'] + 8 * config['scale_factor']) + available_height = config['height'] - data_start_y - int(60 * config['scale_factor']) + visible_rows = min(len(sorted_data), available_height // row_height) + + for i in range(visible_rows): + record = sorted_data[i] + row_y = data_start_y + i * row_height + + # Draw row background + row_color = config['colors']['row_alt'] if i % 2 == 0 else config['colors']['row_normal'] + row_rect = (int(5 * config['scale_factor']), row_y, + config['width'] - int(10 * config['scale_factor']), row_height) + pygame.draw.rect(overlay_surface, row_color, row_rect) + + # Function name + name = addr_to_symbol(symbols, record['address']) if symbols else "" + max_name_chars = int(col_widths[0] // (11 * config['scale_factor'])) + display_name = name if len(name) <= max_name_chars else name[:max_name_chars - 3] + "..." + + row_data = [display_name] + + # Event data + for j, event_count in enumerate(record['events']): + event_scale = "" + event_count //= max(1, record['call_count']) + if event_count > 1_000_000_000: + event_count //= 1_000_000_000 + event_scale = "B" + elif event_count > 1_000_000: + event_count //= 1_000_000 + event_scale = "M" + row_data.append(f"{event_count:,}{event_scale}") + + # Determine row color based on sorting key (event 0) + if len(record['events']) > 0 and event_totals[0] > 0: + sort_key_value = record['events'][0] // max(1, record['call_count']) + percentage = (sort_key_value / event_totals[0] * 100) + row_text_color = get_color_by_percentage(percentage, config['colors']['content_text']) + else: + row_text_color = config['colors']['content_text'] + + # Draw row data with uniform color + current_x = 10 + for j, (data, width) in enumerate(zip(row_data, col_widths)): + text_surface = config['fonts']['content'].render(str(data), True, row_text_color) + overlay_surface.blit(text_surface, (current_x, row_y + int(8 * config['scale_factor']))) + + if j < len(row_data) - 1: + sep_x = current_x + width - 8 + pygame.draw.line(overlay_surface, (60, 70, 85), + (sep_x, row_y), (sep_x, row_y + row_height), 1) + current_x += width + + # Draw summary + summary_y = config['height'] - int(50 * config['scale_factor']) + total_functions = len(profile_data) + grand_total = sum(event_totals) + summary_text = ( + f"Profiles: {total_functions} | " + f"Events: {num_events} | " + f"Total Events: {grand_total:,}" + ) + + summary_surface = config['fonts']['summary'].render(summary_text, True, config['colors']['content_text']) + summary_rect = summary_surface.get_rect() + summary_x = (config['width'] - summary_rect.width) // 2 + overlay_surface.blit(summary_surface, (summary_x, summary_y)) + + # Instructions + instruction_str = "Press 'P' to toggle event counter overlay" + instruction_text = config['fonts']['instruction'].render(instruction_str, True, (180, 180, 180)) + overlay_surface.blit(instruction_text, (0, summary_y + int(20 * config['scale_factor']))) + + +def draw_profile_table(overlay_surface, config, profile_data, profile_mode, symbols): + """Draw the profile mode table.""" + + # Prepare data + sort_func = lambda x: x['total_ticks'] + sorted_data = sorted(profile_data, key=sort_func, reverse=True) + total_ticks_all = sum(record['total_ticks'] for record in profile_data) + profile_mode = "Exclusive" if profile_mode else "Inclusive" + + headers = ["Function", "Calls", "Min", "Max", "Total", "Avg", "Cycles", "%"] + proportions = [0.30, 0.08, 0.10, 0.10, 0.13, 0.10, 0.13, 0.05] + col_widths = [config['width'] * prop for prop in proportions] + + # Draw table structure + draw_table(overlay_surface, config, f"Performance Profile ({profile_mode})", headers, col_widths) + + # Draw data rows + row_height = int(30 * config['scale_factor']) + data_start_y = int(50 * config['scale_factor'] + 40 * config['scale_factor'] + 8 * config['scale_factor']) + available_height = config['height'] - data_start_y - int(60 * config['scale_factor']) + visible_rows = min(len(sorted_data), available_height // row_height) + + for i in range(visible_rows): + record = sorted_data[i] + row_y = data_start_y + i * row_height + + # Draw row background + row_color = config['colors']['row_alt'] if i % 2 == 0 else config['colors']['row_normal'] + row_rect = (int(5 * config['scale_factor']), row_y, + config['width'] - int(10 * config['scale_factor']), row_height) + pygame.draw.rect(overlay_surface, row_color, row_rect) + + # Function name + name = addr_to_symbol(symbols, record['address']) if symbols else "" + max_name_chars = int(col_widths[0] // (11 * config['scale_factor'])) + display_name = name if len(name) <= max_name_chars else name[:max_name_chars - 3] + "..." + + # Calculate values + call_count = record['call_count'] + min_ticks = record['min_ticks'] if call_count else 0 + max_ticks = record['max_ticks'] if call_count else 0 + total_ticks = record['total_ticks'] + avg_cycles = record['total_cycles'] // max(1, call_count) + avg_ticks = total_ticks // max(1, call_count) + percentage = (total_ticks / total_ticks_all * 100) + + ticks_scale = "" + if total_ticks > 1_000_000_000: + total_ticks //= 1_000_000 + ticks_scale = "M" + + row_data = [ + display_name, + f"{call_count:,}", + f"{min_ticks:,}", + f"{max_ticks:,}", + f"{total_ticks:,}{ticks_scale}", + f"{avg_ticks:,}", + f"{avg_cycles:,}", + f"{percentage:.1f}%" + ] + + # Determine row color based on percentage + text_color = get_color_by_percentage(percentage, config['colors']['content_text']) + + # Draw row data + current_x = int(10 * config['scale_factor']) + for j, (data, width) in enumerate(zip(row_data, col_widths)): + text_surface = config['fonts']['content'].render(str(data), True, text_color) + overlay_surface.blit(text_surface, (current_x, row_y + int(8 * config['scale_factor']))) + + if j < len(row_data) - 1: + sep_x = current_x + width - int(8 * config['scale_factor']) + pygame.draw.line(overlay_surface, (60, 70, 85), + (sep_x, row_y), (sep_x, row_y + row_height), 1) + current_x += width + + # Draw summary + summary_y = config['height'] - int(50 * config['scale_factor']) + total_calls = sum(record['call_count'] for record in profile_data) + total_cycles = sum(record['total_cycles'] for record in profile_data) + total_ticks_summary = sum(record['total_ticks'] for record in profile_data) + + summary_text = ( + f"Profiles: {len(profile_data)} | " + f"Total Calls: {total_calls:,} | " + f"Total Ticks: {total_ticks_summary:,} | " + f"Total Cycles: {total_cycles:,}" + ) + + summary_surface = config['fonts']['summary'].render(summary_text, True, config['colors']['content_text']) + summary_rect = summary_surface.get_rect() + summary_x = (config['width'] - summary_rect.width) // 2 + overlay_surface.blit(summary_surface, (summary_x, summary_y)) + + # Instructions + instruction_str = "Press 'P' to toggle event counter overlay" + instruction_text = config['fonts']['instruction'].render(instruction_str, True, (180, 180, 180)) + overlay_surface.blit(instruction_text, (0, summary_y + int(20 * config['scale_factor']))) + +def draw_profile_overlay(screen, screen_width, screen_height, profile_data, + profile_mode, profile_type, scale, symbols, alpha=250): + """Main entry point for drawing the profile overlay.""" + # Calculate dimensions and create surface + base_width, base_height = 800, 800 + screen_width *= scale + screen_height *= scale + scale_factor = min(screen_width / base_width, screen_height / base_height) + + overlay_surface = pygame.Surface((screen_width, screen_height), pygame.SRCALPHA) + overlay_surface.set_alpha(alpha) + + # Setup common configuration + config = { + 'width': screen_width, + 'height': screen_height, + 'scale_factor': scale_factor, + 'colors': { + 'bg': (40, 50, 65), + 'border': (70, 80, 100), + 'header_bg': (60, 80, 120), + 'header_text': (255, 255, 255), + 'content_text': (220, 220, 220), + 'row_alt': (35, 45, 60), + 'row_normal': (45, 55, 70) + }, + 'fonts': { + 'title': pygame.font.SysFont("arial", int(28 * scale_factor), bold=True), + 'header': pygame.font.SysFont("monospace", int(20 * scale_factor), bold=True), + 'content': pygame.font.SysFont("monospace", int(18 * scale_factor)), + 'summary': pygame.font.SysFont("arial", int(20 * scale_factor)), + 'instruction': pygame.font.SysFont("arial", int(22 * scale_factor)) + } + } + + # Draw based on mode + if profile_type == 1: + draw_profile_table(overlay_surface, config, profile_data, profile_mode, symbols) + elif profile_type == 2: + draw_event_table(overlay_surface, config, profile_data, profile_mode, symbols) + + screen.blit(overlay_surface, (0, 0)) + +def pygame_test(port, script, poll_rate, scale, benchmark, symbols): # init pygame pygame.init() pyopenmv.disconnect() @@ -67,27 +415,63 @@ def pygame_test(port, poll_rate, scale, benchmark): pyopenmv.set_timeout(1*2) # SD Cards can cause big hicups. pyopenmv.stop_script() pyopenmv.enable_fb(True) - pyopenmv.exec_script(bench_script if benchmark else test_script) + pyopenmv.reset_profiler() + + # Configure some event counters. + pyopenmv.set_event_counter(0, 0x0039) + pyopenmv.set_event_counter(1, 0x0023) + pyopenmv.set_event_counter(2, 0x0024) + pyopenmv.set_event_counter(3, 0x0001) + pyopenmv.set_event_counter(4, 0x0003) + pyopenmv.set_event_counter(5, 0xC102) + pyopenmv.set_event_counter(6, 0x02CC) + pyopenmv.set_event_counter(7, 0xC303) + + pyopenmv.exec_script(script) # init screen running = True screen = None + + # Profiling control + profile_type = 0 + profile_mode = 0 + profile_data = [] + last_profile_read = 0 clock = pygame.time.Clock() fps_clock = pygame.time.Clock() font = pygame.font.SysFont("monospace", 30) - if benchmark: + if not benchmark: + pygame.display.set_caption("OpenMV Camera") + else: + pygame.display.set_caption("OpenMV Camera (Benchmark)") screen = pygame.display.set_mode((640, 120), pygame.DOUBLEBUF, 32) try: while running: # Read state - w, h, data, size, text, fmt = pyopenmv.read_state() + w, h, data, size, text, fmt, profiling = pyopenmv.read_state() if text is not None: print(text, end="") + # Read profiling data (maximum 10Hz) + if profiling and profile_type: + current_time = time.time() + if current_time - last_profile_read >= 0.1: # 10Hz = 0.1s interval + tmp_data = pyopenmv.read_profile() + if tmp_data: + profile_data = tmp_data + last_profile_read = current_time + + #if profile_data: + # for r in profile_data: + # print(f"Func: {addr_to_symbol(symbols, r['address'])}@0x{r['address']:x} ") + # print(f"Call: {addr_to_symbol(symbols, r['caller'])}@0x{r['caller']:x}") + # sys.exit(0) + if data is not None: fps = fps_clock.get_fps() @@ -104,7 +488,14 @@ def pygame_test(port, poll_rate, scale, benchmark): screen.fill((0, 0, 0)) else: screen.blit(image, (0, 0)) - screen.blit(font.render(f"{fps:.2f} FPS {fps * size / 1024**2:.2f} MB/s {w}x{h} {fmt}", 5, (255, 0, 0)), (0, 0)) + + # FPS text + fps_text = f"{fps:.2f} FPS {fps * size / 1024**2:.2f} MB/s {w}x{h} {fmt}" + screen.blit(font.render(fps_text, 5, (255, 0, 0)), (0, 0)) + + # Draw profile overlay if enabled + if profile_type and profile_data: + draw_profile_overlay(screen, w, h, profile_data, profile_mode, profile_type, scale, symbols) # update display pygame.display.flip() @@ -116,10 +507,18 @@ def pygame_test(port, poll_rate, scale, benchmark): elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False - if event.key == pygame.K_c: + elif event.key == pygame.K_c: pygame.image.save(image, "capture.png") - + elif event.key == pygame.K_p: + profile_type = (profile_type + 1) % 3 + elif event.key == pygame.K_m: + profile_mode = not profile_mode + pyopenmv.set_profile_mode(profile_mode) + elif event.key == pygame.K_r: + pyopenmv.reset_profiler() + clock.tick(1000//poll_rate) + except KeyboardInterrupt: pass @@ -128,9 +527,38 @@ def pygame_test(port, poll_rate, scale, benchmark): if __name__ == '__main__': parser = argparse.ArgumentParser(description='pyopenmv module') - parser.add_argument('--port', action = 'store', help='OpenMV camera port (default /dev/ttyACM0)', default='/dev/ttyACM0', ) + parser.add_argument('--port', action = 'store', help='Serial port (dev/ttyACM0)', default='/dev/ttyACM0') + parser.add_argument("--script", action = "store", default=None, help = "Script file") parser.add_argument('--poll', action = 'store', help='Poll rate (default 4ms)', default=4, type=int) parser.add_argument('--bench', action = 'store_true', help='Run throughput benchmark.', default=False) parser.add_argument('--scale', action = 'store', help='Set frame scaling factor (default 4x).', default=4, type=int) + parser.add_argument('--firmware', action = 'store', help='Firmware for address to symbol', default=None) + args = parser.parse_args() - pygame_test(args.port, args.poll, args.scale, args.bench) + if args.script is not None: + with open(args.script) as f: + args.script = f.read() + else: + args.script = bench_script if args.bench else test_script + + symbols = [] + + if args.firmware: + from elftools.elf.elffile import ELFFile + + with open(args.firmware, 'rb') as f: + elf = ELFFile(f) + symtab = elf.get_section_by_name('.symtab') + if not symtab: + raise ValueError("No symbol table found in ELF.") + + for sym in symtab.iter_symbols(): + addr = sym['st_value'] + size = sym['st_size'] + name = sym.name + if name and size > 0: # ignore empty symbols + symbols.append((addr, addr + size, name)) + + symbols.sort() + + pygame_test(args.port, args.script, args.poll, args.scale, args.bench, symbols)