diff --git a/src/headset/remote_headset.cpp b/src/headset/remote_headset.cpp
index 0697bf91b8edc932c1bf5170274aa0231eb027a6..07dc88bc4e8220df7dfb2b29b9a9f185a0a7c7b0 100644
--- a/src/headset/remote_headset.cpp
+++ b/src/headset/remote_headset.cpp
@@ -37,21 +37,28 @@ bool RemoteHeadset::on_setup_device(lava::device::create_param& parameters)
 
 bool RemoteHeadset::on_create()
 {
-    this->statistic_send_bitrate = make_statistic("Send-Bitrate (Mbps)", 128);
-    this->statistic_send_queue_size = make_statistic("Send-Queue (KBytes)", 128);
-    this->statistic_receive_bitrate = make_statistic("Receive-Bitrate (Mbps)", 128);
-    this->statistic_receive_queue_size = make_statistic("Receive-Queue (KBytes)", 128);
+    StatisticLog::Ptr statistic_log = this->get_application()->get_statistic_log();
+
+    this->statistic_send_bitrate = make_statistic("host_send_bitrate", UNIT_MBITS_PER_SECOND);
+    this->statistic_send_queue_size = make_statistic("host_send_queue", UNIT_KBITS);
+    this->statistic_receive_bitrate = make_statistic("host_receive_bitrate", UNIT_MBITS_PER_SECOND);
+    this->statistic_receive_queue_size = make_statistic("host_receive_queue", UNIT_KBITS);
     this->statistic_latency = make_latency_statistic("latency");
 
-    this->graph_list.push_back(this->statistic_latency->get_total_latency());
-    this->graph_list.push_back(this->statistic_latency->get_server_response());
-    this->graph_list.push_back(this->statistic_latency->get_frame_decoded());
-    this->graph_list.push_back(this->statistic_latency->get_command_construct());
-    this->graph_list.push_back(this->statistic_latency->get_command_executed());
-    this->graph_list.push_back(this->statistic_send_bitrate);
-    this->graph_list.push_back(this->statistic_send_queue_size);
-    this->graph_list.push_back(this->statistic_receive_bitrate);
-    this->graph_list.push_back(this->statistic_receive_queue_size);
+    statistic_log->add_statistic(this->statistic_send_bitrate);
+    statistic_log->add_statistic(this->statistic_send_queue_size);
+    statistic_log->add_statistic(this->statistic_receive_bitrate);
+    statistic_log->add_statistic(this->statistic_receive_queue_size);
+
+    this->statistic_list.push_back(this->statistic_latency->get_total_latency());
+    this->statistic_list.push_back(this->statistic_latency->get_server_response());
+    this->statistic_list.push_back(this->statistic_latency->get_frame_decoded());
+    this->statistic_list.push_back(this->statistic_latency->get_command_construct());
+    this->statistic_list.push_back(this->statistic_latency->get_command_executed());
+    this->statistic_list.push_back(this->statistic_send_bitrate);
+    this->statistic_list.push_back(this->statistic_send_queue_size);
+    this->statistic_list.push_back(this->statistic_receive_bitrate);
+    this->statistic_list.push_back(this->statistic_receive_queue_size);
 
     StereoStrategy::Ptr strategy = this->get_application()->get_stereo_strategy();
     this->frame_id_count = strategy->get_max_remote_frame_ids();
@@ -128,14 +135,25 @@ bool RemoteHeadset::on_interface()
         this->transport->send_overlay_config(this->overlay_enable);
     }
 
-    std::vector<const char*> graph_names;
+    std::vector<std::string> graph_names;
+    std::vector<const char*> graph_name_pointers;
 
-    for (const Statistic::Ptr& graph : this->graph_list)
+    std::unique_lock<std::mutex> lock(this->transport_mutex);
+    for (const Statistic::Ptr& statistic : this->statistic_list)
     {
-        graph_names.push_back(graph->get_name().c_str());
+        if (!statistic->is_log_only())
+        {
+            graph_names.push_back(statistic->get_display_name());
+        }
     }
+    lock.unlock();
 
-    ImGui::Combo("Overlay Graph", (int32_t*) &this->overlay_graph, graph_names.data(), graph_names.size());
+    for (const std::string& graph_name : graph_names)
+    {
+        graph_name_pointers.push_back(graph_name.c_str());
+    }
+
+    ImGui::Combo("Overlay Graph", (int32_t*) &this->overlay_graph, graph_name_pointers.data(), graph_name_pointers.size());
     
     ImGui::Separator();
 
@@ -222,14 +240,14 @@ bool RemoteHeadset::on_interface()
 
     ImGui::Separator();
 
-    this->statistic_send_bitrate->interface();
-    this->statistic_receive_bitrate->interface();
-
-    this->statistic_send_queue_size->interface();
-    this->statistic_receive_queue_size->interface();
-
-    std::unique_lock<std::mutex> lock(this->transport_mutex);
-    this->statistic_latency->interface();
+    lock.lock();
+    for (Statistic::Ptr statistic : this->statistic_list)
+    {
+        if (!statistic->is_log_only())
+        {
+            statistic->interface(128);
+        }
+    }
     lock.unlock();
 
     return true;
@@ -399,7 +417,7 @@ bool RemoteHeadset::create_transport()
     {
         this->on_latency(frame_number, frame_id, transform_id, timestamp_transfrom_query, timestamp_server_response, timestamp_frame_decoded, timestamp_command_construct, timestamp_command_executed);
     });
-    this->transport->set_on_performance_sample([this](FrameNumber frame_number, FrameId frame_id, TransformId transform_id, PerformanceSampleUnit unit, bool log_only, double sample, const std::string& name)
+    this->transport->set_on_performance_sample([this](std::optional<FrameNumber> frame_number, std::optional<FrameId> frame_id, std::optional<TransformId> transform_id, Unit unit, bool log_only, double sample, const std::string& name)
     {
         this->on_performance_sample(frame_number, frame_id, transform_id, unit, log_only, sample, name);
     });
@@ -587,9 +605,19 @@ void RemoteHeadset::update_overlay()
 
     if (this->overlay_enable)
     {
+        std::vector<Statistic::Ptr> graph_list;
+
+        for (Statistic::Ptr statistic : this->statistic_list)
+        {
+            if (!statistic->is_log_only())
+            {
+                graph_list.push_back(statistic);
+            }
+        }
+
         if (buttons[BUTTON_B] == BUTTON_STATE_PRESSED && !this->overlay_graph_pressed)
         {
-            this->overlay_graph = (this->overlay_graph + 1) % this->graph_list.size();
+            this->overlay_graph = (this->overlay_graph + 1) % graph_list.size();
             this->overlay_graph_pressed = true;
         }
 
@@ -598,10 +626,11 @@ void RemoteHeadset::update_overlay()
             this->overlay_graph_pressed = false;
         }
 
-        Statistic::Ptr active_graph = this->graph_list[this->overlay_graph];
-        std::vector<float> samples = active_graph->get_samples_ordered();
+        Statistic::Ptr active_graph = graph_list[this->overlay_graph];
+        std::vector<float> samples = active_graph->get_sample_values(128);
+        std::string label = active_graph->get_label_name();
 
-        this->transport->send_overlay_graph(this->frame_number, 0, active_graph->get_name(), std::span(samples));
+        this->transport->send_overlay_graph(this->frame_number, 0, label, std::span(samples));
         this->transport->send_overlay_text(this->frame_number, 1, "Headset", this->get_name());
         this->transport->send_overlay_text(this->frame_number, 2, "Strategy", this->get_application()->get_stereo_strategy()->get_name());
     }
@@ -679,9 +708,37 @@ void RemoteHeadset::on_latency(FrameNumber frame_number, FrameId frame_id, Trans
     lock.unlock();
 }
 
-void RemoteHeadset::on_performance_sample(FrameNumber frame_number, FrameId frame_id, TransformId transform_id, PerformanceSampleUnit unit, bool log_only, double sample, const std::string& name)
+void RemoteHeadset::on_performance_sample(std::optional<FrameNumber> frame_number, std::optional<FrameId> frame_id, std::optional<TransformId> transform_id, Unit unit, bool log_only, double sample, const std::string& name)
 {
-    
+    std::unique_lock<std::mutex> lock(this->transport_mutex);
+    Statistic::Ptr statistic;
+
+    for (uint32_t index = 0; index < this->statistic_list.size(); index++)
+    {
+        if (this->statistic_list[index]->get_name() == name)
+        {
+            statistic = this->statistic_list[index];
+
+            break;
+        }
+    }
+
+    if (statistic == nullptr)
+    {
+        statistic = make_statistic(name, unit, log_only);
+        this->statistic_list.push_back(statistic);
+        
+        StatisticLog::Ptr statistic_log = this->get_application()->get_statistic_log();
+        statistic_log->add_statistic(statistic);
+    }
+
+    StatisticSample statistic_sample;
+    statistic_sample.value = sample;
+    statistic_sample.frame_number = frame_number;
+    statistic_sample.frame_id = frame_id;
+    statistic_sample.transform_id = transform_id;
+
+    statistic->add_sample(statistic_sample);
 }
 
 void RemoteHeadset::on_transport_error()
diff --git a/src/headset/remote_headset.hpp b/src/headset/remote_headset.hpp
index d65f190073d448ec32f13943fe9653976bc9d135..aa34cb9f68eb4d616b963592c6ee42c792f7308d 100644
--- a/src/headset/remote_headset.hpp
+++ b/src/headset/remote_headset.hpp
@@ -71,7 +71,7 @@ private:
     void on_controller_button(Controller controller, Button button, ButtonState button_state);
     void on_controller_thumbstick(Controller controller, const glm::vec2& thumbstick);
     void on_latency(FrameNumber frame_number, FrameId frame_id, TransformId transform_id, double timestamp_transform_query, double timestamp_server_response, double timestamp_frame_decoded, double timestamp_command_construct, double timestamp_command_executed);
-    void on_performance_sample(FrameNumber frame_number, FrameId frame_id, TransformId transform_id, PerformanceSampleUnit unit, bool log_only, double sample, const std::string& name);
+    void on_performance_sample(std::optional<FrameNumber> frame_number, std::optional<FrameId> frame_id, std::optional<TransformId> transform_id, Unit unit, bool log_only, double sample, const std::string& name);
     void on_transport_error();
     void on_encode_error();
 
@@ -128,8 +128,8 @@ private:
     bool overlay_enable_pressed = false;
     uint32_t overlay_graph = 0;
     bool overlay_graph_pressed = false;
-    std::vector<Statistic::Ptr> graph_list;
 
+    std::vector<Statistic::Ptr> statistic_list;                                             //NOTE: Protected by transport_mutex
     Statistic::Ptr statistic_send_bitrate;
     Statistic::Ptr statistic_send_queue_size;
     Statistic::Ptr statistic_receive_bitrate;
diff --git a/src/transport/transport.hpp b/src/transport/transport.hpp
index a2022a24b3aaef1355cced63240da2ebc3ece797..4363aafe6d3a6b1c7b22b4358933a48f71ae2bd0 100644
--- a/src/transport/transport.hpp
+++ b/src/transport/transport.hpp
@@ -26,6 +26,7 @@
 #include <glm/glm.hpp>
 #include <functional>
 #include <memory>
+#include <optional>
 #include <span>
 
 #include "types.hpp"
@@ -100,7 +101,7 @@ public:
 
 
 
-    typedef std::function<void(FrameNumber frame_number, FrameId frame_id, TransformId transform_id, PerformanceSampleUnit unit, bool log_only, double sample, const std::string& name)> OnPerformanceSample;
+    typedef std::function<void(std::optional<FrameNumber> frame_number, std::optional<FrameId> frame_id, std::optional<TransformId> transform_id, Unit unit, bool log_only, double sample, const std::string& name)> OnPerformanceSample;
 
     // OnTransportError: Called in case of error or disconnect. After that the transport method transitions to the error state
     typedef std::function<void()> OnTransportError;
diff --git a/src/transport/udp_packet.cpp b/src/transport/udp_packet.cpp
index b7bb3764e26b590a5b718120a0b78c53acf9b092..e47e4272aabffc6bce83fe19628ac83dfe2c92ba 100644
--- a/src/transport/udp_packet.cpp
+++ b/src/transport/udp_packet.cpp
@@ -100,30 +100,30 @@ bool convert_udp_button_state(UDPButtonState udp_button_state, ButtonState& butt
     return true;
 }
 
-bool convert_udp_performance_sample_unit(UDPPerformanceSampleUnit udp_unit, PerformanceSampleUnit& unit)
+bool convert_udp_performance_sample_unit(UDPPerformanceSampleUnit udp_unit, Unit& unit)
 {
     switch (udp_unit)
     {
     case UDP_PERFORMANCE_SAMPLE_UNIT_UNDEFINED:
-        unit = PERFORMANCE_SAMPLE_UNIT_UNDEFINED;
+        unit = UNIT_UNDEFINED;
         break;
     case UDP_PERFORMANCE_SAMPLE_UNIT_BITS:
-        unit = PERFORMANCE_SAMPLE_UNIT_BITS;
+        unit = UNIT_BITS;
         break;
     case UDP_PERFORMANCE_SAMPLE_UNIT_BYTES:
-        unit = PERFORMANCE_SAMPLE_UNIT_BYTES;
+        unit = UNIT_BYTES;
         break;
     case UDP_PERFORMANCE_SAMPLE_UNIT_SECONDS:
-        unit = PERFORMANCE_SAMPLE_UNIT_SECONDS;
+        unit = UNIT_SECONDS;
         break;
     case UDP_PERFORMANCE_SAMPLE_UNIT_MILLISECONDS:
-        unit = PERFORMANCE_SAMPLE_UNIT_MILLISECONDS;
+        unit = UNIT_MILLISECONDS;
         break;
     case UDP_PERFORMANCE_SAMPLE_UNIT_MBITS_PER_SECOND:
-        unit = PERFORMANCE_SAMPLE_UNIT_MBITS_PER_SECOND;
+        unit = UNIT_MBITS_PER_SECOND;
         break;
     case UDP_PERFORMANCE_SAMPLE_UNIT_UNDEFINED_PER_SECOND:
-        unit = PERFORMANCE_SAMPLE_UNIT_UNDEFINED_PER_SECOND;
+        unit = UNIT_UNDEFINED_PER_SECOND;
         break;
     default:
         lava::log()->error("UDP-Transport: Unkown performance sample unit!");
diff --git a/src/transport/udp_packet.hpp b/src/transport/udp_packet.hpp
index 083a7e3c7ffc7afe278827d39dbdb65fef91a8e0..39e8b6b51e0c62bfe5d175e7b85c27830eefa82e 100644
--- a/src/transport/udp_packet.hpp
+++ b/src/transport/udp_packet.hpp
@@ -10,6 +10,10 @@
 #define UDP_PACKET_OVERLAY_GRAPH_LABEL_MAX_LENGTH     128
 #define UDP_PACKET_OVERLAY_GRAPH_SAMPLE_MAX_COUNT     64
 
+#define UDP_UNDEFINED_FRAME_NUMBER 0xFFFFFFFF   //NOTE: Only allowed for PacketPerformanceSample
+#define UDP_UNDEFINED_FRAME_ID     0xFFFFFFFF   //NOTE: Only allowed for PacketPerformanceSample
+#define UDP_UNDEFINED_TRANSFORM_ID 0xFFFFFFFF   //NOTE: Only allowed for PacketPerformanceSample
+
 enum UDPPacketId : uint32_t
 {
     UDP_PACKET_ID_SETUP                 = 0x00,
@@ -88,7 +92,7 @@ bool convert_udp_controller(UDPControllerId udp_controller, Controller& controll
 bool convert_udp_controller_state(UDPControllerState udp_controller_state, ControllerState& controller_state);
 bool convert_udp_button(UDPButtonId udp_button, Button& button);
 bool convert_udp_button_state(UDPButtonState udp_button_state, ButtonState& button_state);
-bool convert_udp_performance_sample_unit(UDPPerformanceSampleUnit udp_unit, PerformanceSampleUnit& unit);
+bool convert_udp_performance_sample_unit(UDPPerformanceSampleUnit udp_unit, Unit& unit);
 
 //All packages need to be smaller than 512, otherwise the package needs to be fragmented.
 //In case of an fragmented package, all parts of it are gathered and sorted based on the information provide in the packet.
diff --git a/src/transport/udp_transport.cpp b/src/transport/udp_transport.cpp
index 32e7ba2a88b1fee0755309fb389c4a5d5a60b797..f71f7969cb1c8a43626aea1888f44319c352306b 100644
--- a/src/transport/udp_transport.cpp
+++ b/src/transport/udp_transport.cpp
@@ -993,7 +993,26 @@ void UDPTransport::parse_performance_sample(const Datagram& datagram)
         return;
     }
 
-    PerformanceSampleUnit unit;
+    std::optional<FrameNumber> frame_number;
+    std::optional<FrameId> frame_id;
+    std::optional<TransformId> transform_id;
+
+    if (packet->frame_number != UDP_UNDEFINED_FRAME_NUMBER)
+    {
+        frame_number = packet->frame_number;
+    }
+
+    if (packet->frame_id != UDP_UNDEFINED_FRAME_ID)
+    {
+        frame_id = packet->frame_id;
+    }
+
+    if (packet->transform_id != UDP_UNDEFINED_TRANSFORM_ID)
+    {
+        transform_id = packet->transform_id;
+    }
+
+    Unit unit;
 
     if (!convert_udp_performance_sample_unit(packet->unit, unit))
     {
@@ -1006,5 +1025,5 @@ void UDPTransport::parse_performance_sample(const Datagram& datagram)
     uint32_t name_length = strnlen((const char*)packet->name, sizeof(packet->name));
     std::string name((const char*)packet->name, name_length);
 
-    this->on_performance_sample(packet->frame_number, packet->frame_id, packet->transform_id, unit, packet->log_only, packet->sample, name);
+    this->on_performance_sample(frame_number, frame_id, transform_id, unit, packet->log_only, packet->sample, name);
 }
\ No newline at end of file
diff --git a/src/types.hpp b/src/types.hpp
index a56459342b19eaa1239c4843aa77e1b8b298fcc7..ff03377e3259608af048cf337961abc0d8bb1e6c 100644
--- a/src/types.hpp
+++ b/src/types.hpp
@@ -49,13 +49,26 @@ enum RemoteStrategy
     REMOTE_STRATEGY_NATIVE
 };
 
-enum PerformanceSampleUnit
+enum Unit
 {
-    PERFORMANCE_SAMPLE_UNIT_UNDEFINED,
-    PERFORMANCE_SAMPLE_UNIT_BITS,
-    PERFORMANCE_SAMPLE_UNIT_BYTES,
-    PERFORMANCE_SAMPLE_UNIT_SECONDS,
-    PERFORMANCE_SAMPLE_UNIT_MILLISECONDS,
-    PERFORMANCE_SAMPLE_UNIT_MBITS_PER_SECOND,
-    PERFORMANCE_SAMPLE_UNIT_UNDEFINED_PER_SECOND
+    UNIT_UNDEFINED,
+    UNIT_HOURS,
+    UNIT_MINUTES,
+    UNIT_SECONDS,
+    UNIT_MILLISECONDS,
+    UNIT_MIRCOSECONDS,
+    UNIT_BITS,
+    UNIT_KBITS,                 //NOTE: Base 1000
+    UNIT_MBITS,                 //NOTE: Base 1000
+    UNIT_KIBITS,                //NOTE: Base 1024
+    UNIT_MIBITS,                //NOTE: Base 1024
+    UNIT_BYTES,
+    UNIT_KBYTES,                //NOTE: Base 1000
+    UNIT_MBYTES,                //NOTE: Base 1000
+    UNIT_KIBYTES,               //NOTE: Base 1024
+    UNIT_MIBYTES,               //NOTE: Base 1024
+    UNIT_UNDEFINED_PER_SECOND,
+    UNIT_BITS_PER_SECOND,
+    UNIT_KBITS_PER_SECOND,      //NOTE: Base 1000
+    UNIT_MBITS_PER_SECOND,      //NOTE: Base 1000
 };
\ No newline at end of file
diff --git a/src/utility/latency.cpp b/src/utility/latency.cpp
index 83b7a2deb766fe0fafb348323ed3e208e84b2702..87d9f3a0e2586912eeb5a24e32091d025c87e2aa 100644
--- a/src/utility/latency.cpp
+++ b/src/utility/latency.cpp
@@ -6,20 +6,20 @@
 
 LatencyStatistic::LatencyStatistic(const std::string& base_directory) : base_directory(base_directory)
 {
-    this->statistic_total_latency = make_statistic("Total Latency", 128);
-    this->statistic_server_response = make_statistic("Server Response", 128);
-    this->statistic_frame_decoded = make_statistic("Frame Decoded", 128);
-    this->statistic_command_construct = make_statistic("Command Construct", 128);
-    this->statistic_command_executed = make_statistic("Command Executed", 128);
+    this->statistic_total_latency = make_statistic("Total Latency", UNIT_MILLISECONDS);
+    this->statistic_server_response = make_statistic("Server Response", UNIT_MILLISECONDS);
+    this->statistic_frame_decoded = make_statistic("Frame Decoded", UNIT_MILLISECONDS);
+    this->statistic_command_construct = make_statistic("Command Construct", UNIT_MILLISECONDS);
+    this->statistic_command_executed = make_statistic("Command Executed", UNIT_MILLISECONDS);
 }
 
 void LatencyStatistic::interface()
 {
-    this->statistic_total_latency->interface();
-    this->statistic_server_response->interface();
-    this->statistic_frame_decoded->interface();
-    this->statistic_command_construct->interface();
-    this->statistic_command_executed->interface();
+    this->statistic_total_latency->interface(128);
+    this->statistic_server_response->interface(128);
+    this->statistic_frame_decoded->interface(128);
+    this->statistic_command_construct->interface(128);
+    this->statistic_command_executed->interface(128);
 }
 
 bool LatencyStatistic::write_to_file()
diff --git a/src/utility/pass_timer.cpp b/src/utility/pass_timer.cpp
index c13b0f3ca5c2bfeb74e108c632c499bbded4766d..4f9fe8bc50196e9bce66f589fd16bc6fb2d2b82a 100644
--- a/src/utility/pass_timer.cpp
+++ b/src/utility/pass_timer.cpp
@@ -72,7 +72,7 @@ void PassTimer::interface()
 {
     for (Statistic::Ptr statistic : this->pass_statistcs)
     {
-        statistic->interface();
+        statistic->interface(128);
     }
 }
 
@@ -287,7 +287,7 @@ bool PassTimer::evaluate_wating_passes(lava::device_ptr device, lava::index fram
 
         if (!found)
         {
-            Statistic::Ptr statistic = make_statistic(pass_statistic.name, 128);
+            Statistic::Ptr statistic = make_statistic(pass_statistic.name, UNIT_MILLISECONDS);
             statistic->add_sample(pass_statistic.delta_time);
 
             this->pass_statistcs.push_back(statistic);
diff --git a/src/utility/statistic.cpp b/src/utility/statistic.cpp
index 5a272aaec758e86d2952d00d23e57f5f6f338447..be57146bc3c8092c1d1fcabdec26cef07ac95e03 100644
--- a/src/utility/statistic.cpp
+++ b/src/utility/statistic.cpp
@@ -1,105 +1,246 @@
 #include "statistic.hpp"
-#include <array>
+#include <liblava/lava.hpp>
+#include <glm/glm.hpp>
 #include <imgui.h>
 
-Statistic::Statistic(const std::string& name, uint32_t sample_count) : name(name)
+#include <filesystem>
+#include <fstream>
+
+bool StatisticKey::operator<(const StatisticKey& key) const
 {
-    this->samples.resize(sample_count, 0.0f);
+    if (this->frame_number == key.frame_number)
+    {
+        return this->transform_id < key.transform_id;
+    }
+
+    return this->frame_number < key.frame_number;
 }
 
-void Statistic::interface()
+Statistic::Statistic(const std::string& name, Unit unit, bool log_only) : name(name), unit(unit), log_only(log_only)
 {
-    std::string plot_name = this->name;
+    
+}
 
-    for (uint32_t index = 0; index < plot_name.size(); index++)
-    {
-        if (index == 0)
-        {
-            plot_name[index] = std::toupper(plot_name[index]);
-        }
+void Statistic::interface(uint32_t sample_count)
+{
+    std::string plot_name = this->get_label_name();
 
-        else if (plot_name[index - 1] == '_')
-        {
-            plot_name[index - 1] = ' ';
-            plot_name[index] = std::toupper(plot_name[index]);
-        }
+    uint32_t plot_length = glm::min(sample_count, (uint32_t)this->samples.size());
+    uint32_t plot_src_offset = this->samples.size() - plot_length;
+    uint32_t plot_dst_offset = sample_count - plot_length;
+
+    std::vector<float> plot_samples;
+    plot_samples.resize(sample_count, 0.0f);
+
+    for (uint32_t index = 0; index < plot_length; index++)
+    {
+        plot_samples[plot_dst_offset + index] = this->samples[plot_src_offset + index].value;
     }
 
-    ImGui::PlotLines(plot_name.c_str(), this->samples.data(), this->samples.size(), this->current_index, nullptr, 0.0f, this->get_max(), ImVec2(0, 64));
+    ImGui::PlotLines(plot_name.c_str(), plot_samples.data(), plot_samples.size(), 0, nullptr, 0.0f, this->get_max(sample_count), ImVec2(0, 64));
 
     ImVec2 cursor = ImGui::GetCursorPos();
     ImGui::SetWindowFontScale(0.7f);
 
     ImGui::SetCursorPosX(cursor.x + 5);
     ImGui::SetCursorPosY(cursor.y - 22.0f);
-    ImGui::Text("Avg: %8.4f", this->get_average());
+    ImGui::Text("Avg: %8.4f", this->get_average(sample_count));
     ImGui::SetCursorPosX(cursor.x + 90);
     ImGui::SetCursorPosY(cursor.y - 22.0f);
-    ImGui::Text("Min: %8.4f", this->get_min());
+    ImGui::Text("Min: %8.4f", this->get_min(sample_count));
     ImGui::SetCursorPosX(cursor.x + 175);
     ImGui::SetCursorPosY(cursor.y - 22.0f);
-    ImGui::Text("Max: %8.4f", this->get_max());
+    ImGui::Text("Max: %8.4f", this->get_max(sample_count));
 
     ImGui::SetCursorPos(cursor);
     ImGui::SetWindowFontScale(1.0);
 }
 
-void Statistic::add_sample(float sample)
+void Statistic::add_sample(float value)
 {
-    this->samples[this->current_index] = sample;
-    this->valid_count = std::max(this->valid_count, this->current_index + 1);
-    this->current_index = (this->current_index + 1) % this->samples.size();
+    StatisticSample sample;
+    sample.value = value;
+
+    this->samples.push_back(sample);
 }
 
-float Statistic::get_average() const
+void Statistic::add_sample(const StatisticSample& sample)
 {
-    if (this->valid_count <= 0)
+    this->samples.push_back(sample);
+}
+
+double Statistic::get_average() const
+{
+    return this->get_average(this->samples.size());
+}
+
+double Statistic::get_average(uint32_t sample_count) const
+{
+    uint32_t count = glm::min(sample_count, (uint32_t)this->samples.size());
+
+    if (count <= 0)
     {
         return 0.0f;
     }
 
-    float sample_sum = 0.0f;
+    uint32_t offset = this->samples.size() - count;
+    double sum = 0.0f;
 
-    for (uint32_t index = 0; index < this->valid_count; index++)
+    for (uint32_t index = offset; index < this->samples.size(); index++)
     {
-        sample_sum += this->samples[index];
+        sum += this->samples[index].value;
     }
 
-    return sample_sum / (float)this->valid_count;
+    return sum / (double)count;
 }
 
-float Statistic::get_min() const
+double Statistic::get_min() const
 {
-    if (this->valid_count <= 0)
+    return this->get_min(this->samples.size());
+}
+
+double Statistic::get_min(uint32_t sample_count) const
+{
+    uint32_t count = glm::min(sample_count, (uint32_t)this->samples.size());
+
+    if (count <= 0)
     {
         return 0.0f;
     }
 
-    float sample_min = this->samples[0];
+    uint32_t offset = this->samples.size() - count;
+    double min = this->samples[offset].value;
 
-    for (uint32_t index = 0; index < this->valid_count; index++)
+    for (uint32_t index = offset + 1; index < this->samples.size(); index++)
     {
-        sample_min = std::min(sample_min, this->samples[index]);
+        min = glm::min(min, this->samples[index].value);
     }
 
-    return sample_min;
+    return min;
 }
 
-float Statistic::get_max() const
+double Statistic::get_max() const
 {
-    if (this->valid_count <= 0)
+    return this->get_max(this->samples.size());
+}
+
+double Statistic::get_max(uint32_t sample_count) const
+{
+    uint32_t count = glm::min(sample_count, (uint32_t)this->samples.size());
+
+    if (count <= 0)
     {
         return 0.0f;
     }
 
-    float sample_max = this->samples[0];
+    uint32_t offset = this->samples.size() - count;
+    double max = this->samples[offset].value;
+
+    for (uint32_t index = offset + 1; index < this->samples.size(); index++)
+    {
+        max = glm::max(max, this->samples[index].value);
+    }
+
+    return max;
+}
+
+std::vector<float> Statistic::get_sample_values(uint32_t sample_count) const
+{
+    std::vector<float> sample_values;
+    sample_values.reserve(sample_count);
 
-    for (uint32_t index = 0; index < this->valid_count; index++)
+    uint32_t count = glm::min(sample_count, (uint32_t) this->samples.size());
+    uint32_t offset = this->samples.size() - count;
+
+    for (uint32_t index = offset; index < this->samples.size(); index++)
     {
-        sample_max = std::max(sample_max, this->samples[index]);
+        sample_values.push_back(this->samples[index].value);
     }
 
-    return sample_max;
+    return sample_values;
+}
+
+std::string Statistic::get_label_name() const
+{
+    return this->get_display_name() + " [" + this->get_unit_name() + "]";
+}
+
+std::string Statistic::get_display_name() const
+{
+    std::string display_name = this->name;
+
+    for (uint32_t index = 0; index < display_name.size(); index++)
+    {
+        if (index == 0)
+        {
+            display_name[index] = std::toupper(display_name[index]);
+        }
+
+        else if (display_name[index - 1] == '_')
+        {
+            display_name[index - 1] = ' ';
+            display_name[index] = std::toupper(display_name[index]);
+        }
+    }
+
+    return display_name;
+}
+
+std::string Statistic::get_unit_name() const
+{
+    switch (this->unit)
+    {
+    case UNIT_UNDEFINED:
+        return "undef";
+    case UNIT_HOURS:
+        return "h";
+    case UNIT_MINUTES:
+        return "min";
+    case UNIT_SECONDS:
+        return "s";
+    case UNIT_MILLISECONDS:
+        return "ms";
+    case UNIT_MIRCOSECONDS:
+        return "us";
+    case UNIT_BITS:
+        return "bit";
+    case UNIT_KBITS:
+        return "kbit";
+    case UNIT_MBITS:
+        return "Mbit";
+    case UNIT_KIBITS:
+        return "Kibit";
+    case UNIT_MIBITS:
+        return "Mibit";
+    case UNIT_BYTES:
+        return "B";
+    case UNIT_KBYTES:
+        return "kB";
+    case UNIT_MBYTES:
+        return "MB";
+    case UNIT_KIBYTES:
+        return "KiB";
+    case UNIT_MIBYTES:
+        return "MiB";
+    case UNIT_UNDEFINED_PER_SECOND:
+        return "undef/s";
+    case UNIT_BITS_PER_SECOND:
+        return "bit/s";
+    case UNIT_KBITS_PER_SECOND:
+        return "kbit/s";
+    case UNIT_MBITS_PER_SECOND:
+        return "Mbit/s";
+    default:
+        lava::log()->error("Unkown unit!");
+        break;
+    }
+
+    return "undef";
+}
+
+const std::vector<StatisticSample>& Statistic::get_samples() const
+{
+    return this->samples;
 }
 
 const std::string& Statistic::get_name() const
@@ -107,34 +248,247 @@ const std::string& Statistic::get_name() const
     return this->name;
 }
 
-const std::vector<float>& Statistic::get_samples() const
+Unit Statistic::get_unit() const
 {
-    return this->samples;
+    return this->unit;
 }
 
-std::vector<float> Statistic::get_samples_ordered() const
+bool Statistic::is_log_only() const
 {
-    std::vector<float> samples_ordered;
-    samples_ordered.resize(this->valid_count, 0.0f);
+    return this->log_only;
+}
+
+bool StatisticLog::write(const std::string& directory)
+{
+    if (!std::filesystem::exists(directory))
+    {
+        std::filesystem::create_directory(directory);
+    }
+
+    std::string file_name = this->build_file_name();
+
+    std::fstream file;
+    file.open(directory + "/" + file_name, std::ios::out);
+
+    if (!file.good())
+    {
+        lava::log()->error("Can't write to file '" + file_name + "' for statistic!");
+
+        return false;
+    }
+
+    std::vector<std::string> ordered_sample_labels;
+    std::map<StatisticKey, std::vector<std::optional<double>>> ordered_samples;
+
+    std::vector<std::string> unordered_sample_labels;
+    std::vector<std::vector<double>> unordered_samples;
+
+    for (const Statistic::Ptr& statistic : this->statistic_list)
+    {
+        for (const StatisticSample& sample : statistic->get_samples())
+        {
+            std::string label;
+
+            if (sample.frame_id.has_value())
+            {
+                label = statistic->get_name() + "_" + std::to_string(sample.frame_id.value()) + " [" + statistic->get_unit_name() + "]";
+            }
+
+            else
+            {
+                label = statistic->get_name() + " [" + statistic->get_unit_name() + "]";
+            }
+
+            bool ordered = sample.frame_number.has_value() || sample.transform_id.has_value();
+            
+            if (ordered)
+            {
+                StatisticKey key;
+                key.frame_number = sample.frame_number.value_or(0);
+                key.transform_id = sample.transform_id.value_or(0);
+                
+                uint32_t column_index = 0;
+                bool column_found = false;
+
+                for (uint32_t index = 0; index < ordered_sample_labels.size(); index++)
+                {
+                    if (ordered_sample_labels[index] == label)
+                    {
+                        column_index = index;
+                        column_found = true;
+
+                        break;
+                    }
+                }
+
+                if (!column_found)
+                {
+                    column_index = ordered_sample_labels.size();
+                    ordered_sample_labels.push_back(label);
+                }
+
+                std::vector<std::optional<double>>& row = ordered_samples[key];
+
+                if (row.size() <= column_index)
+                {
+                    row.resize(column_index + 1);
+                }
+
+                row[column_index] = sample.value;
+            }
+
+            else
+            {
+                uint32_t column_index = 0;
+                bool column_found = false;
+
+                for (uint32_t index = 0; index < unordered_sample_labels.size(); index++)
+                {
+                    if (unordered_sample_labels[index] == label)
+                    {
+                        column_index = index;
+                        column_found = true;
+
+                        break;
+                    }
+                }
+
+                if (!column_found)
+                {
+                    column_index = unordered_sample_labels.size();
+                    unordered_sample_labels.push_back(label);
+                }
+
+                if (unordered_samples.size() <= column_index)
+                {
+                    unordered_samples.resize(column_index + 1);
+                }
+
+                unordered_samples[column_index].push_back(sample.value);
+            }
+        }
+    }
+
+    file << "frame_number";
+    file << "; transform_id";
+
+    for (const std::string& label : ordered_sample_labels)
+    {
+        file << "; " << label;
+    }
+
+    if (!ordered_sample_labels.empty() && !unordered_sample_labels.empty())
+    {
+        file << "; ";
+    }
 
-    if (this->valid_count < this->samples.size())
+    for (const std::string& label : unordered_sample_labels)
     {
-        memcpy(samples_ordered.data(), this->samples.data(), this->valid_count * sizeof(float));
+        file << "; " << label;
     }
 
-    else
+    file << std::endl;
+    file << std::fixed << std::setprecision(3);
+
+    std::map<StatisticKey, std::vector<std::optional<double>>>::iterator row_iter = ordered_samples.begin();
+    uint32_t row_index = 0;
+    bool row_added = true;
+
+    while (row_added)
     {
-        uint32_t count1 = this->samples.size() - this->current_index;
-        uint32_t count2 = this->current_index;
+        row_added = false;
+
+        if (row_iter != ordered_samples.end())
+        {
+            const StatisticKey& key = row_iter->first;
+            const std::vector<std::optional<double>>& row = row_iter->second;
+
+            file << key.frame_number << "; " << key.transform_id;
+
+            for (uint32_t index = 0; index < row.size(); index++)
+            {
+                file << "; ";
+
+                if (row[index].has_value())
+                {
+                    file << row[index].value();
+                }
+            }
+
+            for (uint32_t index = 0; index < (ordered_sample_labels.size() - row.size()); index++)
+            {
+                file << "; ";
+            }
 
-        memcpy(samples_ordered.data(), this->samples.data() + this->current_index, count1 * sizeof(float));
-        memcpy(samples_ordered.data() + count1, this->samples.data(), count2 * sizeof(float));
+            row_added = true;
+            row_iter++;
+        }
+        
+        else
+        {
+            file << " ; ";
+
+            for (uint32_t index = 0; index < ordered_sample_labels.size(); index++)
+            {
+                file << "; ";
+            }
+        }
+
+        if (!ordered_sample_labels.empty() && !unordered_sample_labels.empty())
+        {
+            file << "; ";
+        }
+
+        for (const std::vector<double>& column : unordered_samples)
+        {
+            file << "; ";
+
+            if (row_index < column.size())
+            {
+                file << column[row_index];
+
+                row_added = true;
+            }
+        }
+
+        row_index++;
+
+        file << std::endl;
     }
 
-    return samples_ordered;
+    file.close();
+
+    return true;
+}
+
+void StatisticLog::add_statistic(Statistic::Ptr statistic)
+{
+    this->statistic_list.push_back(statistic);
+}
+
+std::string StatisticLog::build_file_name()
+{
+    time_t system_time = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
+    tm local_time = *localtime(&system_time);
+
+    std::string name = "statistic";
+    name += "_" + std::to_string(local_time.tm_mday);
+    name += "-" + std::to_string(local_time.tm_mon + 1);
+    name += "-" + std::to_string(local_time.tm_year + 1900);
+    name += "_" + std::to_string(local_time.tm_hour);
+    name += "-" + std::to_string(local_time.tm_min);
+    name += "-" + std::to_string(local_time.tm_sec);
+    name += ".csv";
+
+    return name;
+}
+
+Statistic::Ptr make_statistic(const std::string& name, Unit unit, bool log_only)
+{
+    return std::make_shared<Statistic>(name, unit, log_only);
 }
 
-Statistic::Ptr make_statistic(const std::string& name, uint32_t sample_count)
+StatisticLog::Ptr make_statistic_log()
 {
-    return std::make_shared<Statistic>(name, sample_count);
+    return std::make_shared<StatisticLog>();
 }
\ No newline at end of file
diff --git a/src/utility/statistic.hpp b/src/utility/statistic.hpp
index 23b57be95703c8fa2e5d5d6295eea8e299fbf762..27dfde52dfa8329a324e1cb80a7083e1901a5e1d 100644
--- a/src/utility/statistic.hpp
+++ b/src/utility/statistic.hpp
@@ -12,8 +12,32 @@
 #include <memory>
 #include <string>
 #include <vector>
+#include <optional>
 #include <cstdint>
 
+#include "types.hpp"
+
+struct StatisticSample
+{
+    double value;
+
+    std::optional<FrameNumber> frame_number;
+    std::optional<FrameId> frame_id;
+    std::optional<TransformId> transform_id;
+};
+
+struct StatisticKey
+{
+public:
+    FrameNumber frame_number;
+    TransformId transform_id;
+
+public:
+    StatisticKey() = default;
+
+    bool operator<(const StatisticKey& key) const;
+};
+
 class Statistic
 {
 public:
@@ -21,25 +45,57 @@ public:
 
 private:
     std::string name;
-    std::vector<float> samples;
+    Unit unit;
+    bool log_only;
 
-    uint32_t current_index = 0;
-    uint32_t valid_count = 0;
+    std::vector<StatisticSample> samples;
 
 public:
-    Statistic(const std::string& name, uint32_t sample_count);
+    Statistic(const std::string& name, Unit unit, bool log_only);
+
+    void interface(uint32_t sample_count);
+
+    void add_sample(float value);
+    void add_sample(const StatisticSample& sample);
 
-    void interface();
+    double get_average() const;
+    double get_average(uint32_t sample_count) const;
+    double get_min() const;
+    double get_min(uint32_t sample_count) const;
+    double get_max() const;
+    double get_max(uint32_t sample_count) const;
 
-    void add_sample(float sample);
+    std::vector<float> get_sample_values(uint32_t sample_count) const;
 
-    float get_average() const;
-    float get_min() const;
-    float get_max() const;
+    std::string get_label_name() const;
+    std::string get_display_name() const;
+    std::string get_unit_name() const;
 
+    const std::vector<StatisticSample>& get_samples() const;
     const std::string& get_name() const;
-    const std::vector<float>& get_samples() const;
-    std::vector<float> get_samples_ordered() const;
+    Unit get_unit() const;
+
+    bool is_log_only() const;
+};
+
+class StatisticLog
+{
+public:
+    typedef std::shared_ptr<StatisticLog> Ptr;
+
+public:
+    StatisticLog() = default;
+
+    bool write(const std::string& directory);
+
+    void add_statistic(Statistic::Ptr statistic);
+
+private:
+    std::string build_file_name();
+
+private:
+    std::vector<Statistic::Ptr> statistic_list;
 };
 
-Statistic::Ptr make_statistic(const std::string& name, uint32_t sample_count);
\ No newline at end of file
+Statistic::Ptr make_statistic(const std::string& name, Unit unit, bool log_only = false);
+StatisticLog::Ptr make_statistic_log();
\ No newline at end of file
diff --git a/src/vr_application.cpp b/src/vr_application.cpp
index e8390490848dfdca4ed66889b22dc5c530e938fc..af896acb7da712a2ae975c72779835a70d91eb42 100644
--- a/src/vr_application.cpp
+++ b/src/vr_application.cpp
@@ -178,6 +178,11 @@ IndirectCache::Ptr VRApplication::get_indirect_cache() const
     return this->indirect_cache;
 }
 
+StatisticLog::Ptr VRApplication::get_statistic_log() const
+{
+    return this->statistic_log;
+}
+
 const CommandParser& VRApplication::get_command_parser() const
 {
     return this->command_parser;
@@ -453,13 +458,20 @@ bool VRApplication::on_create()
         return false;
     }
 
-    this->frame_time = make_statistic("Frame Time", 128);
+    this->statistic_log = make_statistic_log();
+    this->frame_time = make_statistic("frame_time", UNIT_MILLISECONDS);
+    this->statistic_log->add_statistic(this->frame_time);
 
     return this->create_config();
 }
 
 void VRApplication::on_destroy()
 {  
+    if(!this->statistic_log->write("statistic"))
+    {
+        lava::log()->error("Can't write statistic!");
+    }
+
     this->destroy_config();
 
     if (this->headset != nullptr)
@@ -587,7 +599,7 @@ bool VRApplication::on_interface()
         ImGui::Checkbox("Frame Capture", &capture_enabled);
         this->frame_capture->set_enabled(capture_enabled);
 
-        this->frame_time->interface();
+        this->frame_time->interface(128);
         this->pass_timer->interface();
 
         this->app->draw_about(true);
diff --git a/src/vr_application.hpp b/src/vr_application.hpp
index d1cd77da3371449f666df7e401c68e84cdcaeb4a..63cccf9163b9cb41c4dc0877d6c632297bd146da 100644
--- a/src/vr_application.hpp
+++ b/src/vr_application.hpp
@@ -46,6 +46,8 @@ public:
     ShadowCache::Ptr get_shadow_cache() const;
     IndirectCache::Ptr get_indirect_cache() const;
 
+    StatisticLog::Ptr get_statistic_log() const;
+
     const CommandParser& get_command_parser() const;
 
     lava::device_ptr get_device() const;
@@ -97,5 +99,6 @@ private:
     CommandParser command_parser;
     bool created = false;
 
+    StatisticLog::Ptr statistic_log;
     Statistic::Ptr frame_time;
 };
\ No newline at end of file