Loading...

App Lifecycle

This page explains how a Synapse App starts, runs, and stops so you can write your own applications.

Entry Point

Every Synapse App uses Entrypoint<T>() as its entry point. You do not instantiate your app class directly, AppRunner handles that as part of managing the lifecycle.

#include <synapse-app-sdk/app/app.hpp>
#include "apps/my_app.hpp"

int main(const int, const char**)
{
  return synapse::Entrypoint<app::MyApp>();
}

Lifecycle Phases

When Entrypoint<T>() is called, your app goes through these phases in order:

1. Construction

Your app class is instantiated. The constructor should be lightweight — use setup() for initialization that depends on device state.

class MyApp : public synapse::App {
  public:
    MyApp() = default;
};

2. setup()

Called once before the app starts. Use this to initialize readers, taps, validate app configuration, profiling, and any state your app needs. Return true on success or false to abort startup.

bool MyApp::setup() {
  // Initialize a reader for broadband data from node 1
  const uint32_t broadband_node_id = 1;
  if (!setup_reader(broadband_node_id)) {
    spdlog::error("Failed to set up broadband reader");
    return false;
  }

  // Create an output tap for processed data
  if (!create_tap<synapse::Tensor>("output")) {
    spdlog::error("Failed to create output tap");
    return false;
  }

  // Register labels before enabling profiling
  add_profile("frame_acquisition");
  add_profile("signal_processing");

  // Enable function profiling (reported every 5 seconds)
  enable_function_profiling(std::chrono::milliseconds(5000));

  return true;
}

3. main()

Your main processing loop. This runs in its own thread and should loop while node_running_ is true. When node_running_ becomes false (e.g., the signal chain is stopped or the app receives SIGINT), your loop should exit.

void MyApp::main() {
  while (node_running_) {
    // Wait for data from the reader
    auto messages = data_reader_->receive_multipart();
    if (messages.empty()) {
      std::this_thread::sleep_for(std::chrono::microseconds(1));
      continue;
    }

    // Process each message
    for (auto& message : messages) {
      const auto frame = synapse::parse_protobuf_message<synapse::BroadbandFrame>(
          std::move(message));
      if (!frame.has_value()) continue;

      // Your processing logic here
    }
  }
}

Threading Model

When setup() returns true, AppRunner enables log streaming, sets node_running_ to true, and spins up a dedicated thread for main(). Stopping your SciFi sets node_running_ to false, which exits the loop and joins the thread. If your app uses consumer taps, each runs in its own thread; those threads are stopped when the app shuts down.

Avoid blocking the main() loop. If you need long-running work, spawn your own threads and check node_running_ periodically.

Configuration

Your app can read device configuration at runtime using get_app_config(). This retrieves the application node's configuration from /opt/scifi/config/device.json on the device filesystem:

bool MyApp::setup() {
  synapse::ApplicationNodeConfig app_config;
  if (!get_app_config(
    [](const synapse::ApplicationNodeConfig& config) {
      return config.parameters().size() > 0;
    },
    app_config
  )) {
    spdlog::error("Invalid application configuration");
    return false;
  }

  // Use app_config.parameters() for your app-specific settings
  return true;
}

Signal Handling

AppRunner installs a SIGINT handler. When the process receives SIGINT (e.g., from synapsectl stop), it calls stop() on your app, which sets node_running_ to false and triggers a clean shutdown. You do not need to handle signals yourself.