Repository: dalibo/pev2
Pull request #764 (2025-06-19 08:18:56)

Handle more I/O Timings formats


Patch:
diff --git a/example/src/main.ts b/example/src/main.ts
index c3c8e28..0381f92 100644
--- a/example/src/main.ts
+++ b/example/src/main.ts
@@ -1,10 +1,11 @@
 import { createApp } from "vue"
 import "bootstrap"
 import "bootstrap/dist/css/bootstrap.css"
+import VueTippy from "vue-tippy"
 
 import App from "./App.vue"
 
-createApp(App).mount("#app")
+createApp(App).use(VueTippy, { defaultProps: {theme: "light"}}).mount("#app")
 
 declare global {
   interface Window {
diff --git a/src/components/Diagram.vue b/src/components/Diagram.vue
index 0992abf..6c744e9 100644
--- a/src/components/Diagram.vue
+++ b/src/components/Diagram.vue
@@ -3,9 +3,7 @@ import _ from "lodash"
 import {
   computed,
   inject,
-  nextTick,
   onBeforeMount,
-  onMounted,
   provide,
   reactive,
   ref,
@@ -22,8 +20,6 @@ import DiagramRow from "@/components/DiagramRow.vue"
 import LevelDivider from "@/components/LevelDivider.vue"
 
 import { directive as vTippy } from "vue-tippy"
-import tippy, { createSingleton } from "tippy.js"
-import type { CreateSingletonInstance, Instance } from "tippy.js"
 
 const helpService = new HelpService()
 const getHelpMessage = helpService.getHelpMessage
@@ -42,8 +38,6 @@ const highlightedNodeId = inject(HighlightedNodeIdKey)
 
 // The main plan + init plans (all flatten)
 const plans: Row[][] = [[]]
-let tippyInstances: Instance[] = []
-let tippySingleton!: CreateSingletonInstance
 
 const viewOptions = reactive({
   metric: Metric.time,
@@ -71,29 +65,10 @@ onBeforeMount((): void => {
   }
 })
 
-onMounted((): void => {
-  loadTooltips()
-})
-
 watch(viewOptions, onViewOptionsChanged)
 
 function onViewOptionsChanged() {
   localStorage.setItem("diagramViewOptions", JSON.stringify(viewOptions))
-  nextTick(loadTooltips)
-}
-
-function loadTooltips(): void {
-  if (tippySingleton) {
-    tippySingleton.destroy()
-  }
-  _.each(tippyInstances, (instance) => {
-    instance.destroy()
-  })
-  tippyInstances = tippy(".diagram tr.node")
-  tippySingleton = createSingleton(tippyInstances, {
-    delay: 100,
-    allowHTML: true,
-  })
 }
 
 function flatten(
diff --git a/src/components/DiagramRow.vue b/src/components/DiagramRow.vue
index 707a01f..456da9b 100644
--- a/src/components/DiagramRow.vue
+++ b/src/components/DiagramRow.vue
@@ -11,9 +11,10 @@ import {
   ViewOptionsKey,
 } from "@/symbols"
 import type { IPlan, Node, ViewOptions } from "@/interfaces"
-import { HelpService } from "@/services/help-service"
 import { EstimateDirection, BufferLocation, NodeProp, Metric } from "../enums"
 import LevelDivider from "@/components/LevelDivider.vue"
+import TimeTooltip from "@/components/tooltip/TimeTooltip.vue"
+import IoTooltip from "@/components/tooltip/IoTooltip.vue"
 import useNode from "@/node"
 
 interface Props {
@@ -41,58 +42,16 @@ if (!selectNode) {
 }
 const highlightedNodeId = inject(HighlightedNodeIdKey)
 
-const helpService = new HelpService()
-const getHelpMessage = helpService.getHelpMessage
-
 const _viewOptions = inject(ViewOptionsKey) as ViewOptions
 const {
   buffersByLocationTooltip,
   costTooltip,
   estimateFactorPercent,
   estimateFactorTooltip,
-  ioTooltip,
   nodeName,
   rowsTooltip,
-  timeTooltip,
 } = useNode(plan, node, _viewOptions)
 
-function getTooltipContent(node: Node): string {
-  let content = ""
-  switch (diagramViewOptions.metric) {
-    case Metric.time:
-      content += timeTooltip.value
-      break
-    case Metric.rows:
-      content += rowsTooltip.value
-      break
-    case Metric.estimate_factor:
-      content += estimateFactorTooltip.value
-      break
-    case Metric.cost:
-      content += costTooltip.value
-      break
-    case Metric.buffers:
-      content += buffersByLocationTooltip.value(
-        diagramViewOptions.buffersMetric,
-      )
-      break
-    case Metric.io:
-      content += ioTooltip.value
-
-      if (
-        node[NodeProp.WORKERS_PLANNED] ||
-        node[NodeProp.WORKERS_PLANNED_BY_GATHER]
-      ) {
-        content += `<br><small>${getHelpMessage("io timings parallel")}</small>`
-      }
-      break
-  }
-  if (node[NodeProp.CTE_NAME]) {
-    content += "<br><em>CTE " + node[NodeProp.CTE_NAME] + "</em>"
-  }
-  return content
-}
-
 const scrollTo = inject<(el: Element) => null>("scrollTo")
 
 watch(
@@ -106,19 +65,49 @@ watch(
 </script>
 
 <template>
-  <tr
+  <tippy
     class="no-focus-outline node"
     :class="{
       selected: node.nodeId === selectedNodeId,
       highlight: node.nodeId === highlightedNodeId,
     }"
-    :data-tippy-content="getTooltipContent(node)"
+    tag="tr"
     @mouseenter="highlightedNodeId = node.nodeId"
     @mouseleave="highlightedNodeId = undefined"
     @click.prevent="selectNode(node.nodeId, true)"
-    ref="rootEl"
   >
-    <td class="node-index">
+    <template #content>
+      <template v-if="node[NodeProp.CTE_NAME]">
+        <div>
+          <em>CTE {{ node[NodeProp.CTE_NAME] }} </em>
+        </div>
+      </template>
+      <time-tooltip
+        :node="node"
+        v-if="diagramViewOptions.metric == Metric.time"
+      />
+      <io-tooltip
+        :node="node"
+        v-else-if="diagramViewOptions.metric == Metric.io"
+        exclusive
+        class="mb-0"
+      />
+      <template v-else-if="diagramViewOptions.metric == Metric.rows">
+        <div v-html="rowsTooltip"></div>
+      </template>
+      <template v-else-if="diagramViewOptions.metric == Metric.estimate_factor">
+        <div v-html="estimateFactorTooltip"></div>
+      </template>
+      <template v-else-if="diagramViewOptions.metric == Metric.cost">
+        <div v-html="costTooltip"></div>
+      </template>
+      <template v-else-if="diagramViewOptions.metric == Metric.buffers">
+        <div
+          v-html="buffersByLocationTooltip(diagramViewOptions.buffersMetric)"
+        ></div
+      ></template>
+    </template>
+    <td class="node-index" ref="rootEl">
       <span class="fw-normal small">#{{ node.nodeId }} </span>
     </td>
     <td class="node-type pe-2">
@@ -504,8 +493,8 @@ watch(
         style="height: 5px"
         v-else-if="
           diagramViewOptions.metric == Metric.io &&
-          (plan.content.Plan[NodeProp['IO_READ_TIME']] ||
-            plan.content.Plan[NodeProp['IO_WRITE_TIME']])
+          (plan.content.Plan[NodeProp.SUM_IO_READ_TIME] ||
+            plan.content.Plan[NodeProp.SUM_IO_WRITE_TIME])
         "
       >
         <div
@@ -515,7 +504,7 @@ watch(
           :style="{
             width:
               (Math.round(
-                (node[NodeProp.EXCLUSIVE_IO_READ_TIME] /
+                (node[NodeProp.EXCLUSIVE_SUM_IO_READ_TIME] /
                   plan.planStats?.maxIo) *
                   100,
               ) || 0) + '%',
@@ -531,7 +520,7 @@ watch(
           :style="{
             width:
               (Math.round(
-                (node[NodeProp.EXCLUSIVE_IO_WRITE_TIME] /
+                (node[NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME] /
                   plan.planStats?.maxIo) *
                   100,
               ) || 0) + '%',
@@ -542,5 +531,5 @@ watch(
         ></div>
       </div>
     </td>
-  </tr>
+  </tippy>
 </template>
diff --git a/src/components/Grid.vue b/src/components/Grid.vue
index 3acd40b..0e4454f 100644
--- a/src/components/Grid.vue
+++ b/src/components/Grid.vue
@@ -67,7 +67,12 @@ const hasTime = computed((): boolean => {
 const hasIORead = computed((): boolean => {
   return _.some(plans, (plan: Row[]) => {
     return _.some(plan, (row: Row) => {
-      return row[1][NodeProp.IO_READ_TIME] || 0 > 1
+      return (
+        row[1][NodeProp.IO_READ_TIME] ||
+        row[1][NodeProp.SHARED_IO_READ_TIME] ||
+        row[1][NodeProp.LOCAL_IO_READ_TIME] ||
+        row[1][NodeProp.TEMP_IO_READ_TIME]
+      )
     })
   })
 })
@@ -75,7 +80,12 @@ const hasIORead = computed((): boolean => {
 const hasIOWrite = computed((): boolean => {
   return _.some(plans, (plan: Row[]) => {
     return _.some(plan, (row: Row) => {
-      return row[1][NodeProp.IO_WRITE_TIME] || 0 > 1
+      return (
+        row[1][NodeProp.IO_WRITE_TIME] ||
+        row[1][NodeProp.SHARED_IO_WRITE_TIME] ||
+        row[1][NodeProp.LOCAL_IO_WRITE_TIME] ||
+        row[1][NodeProp.TEMP_IO_WRITE_TIME]
+      )
     })
   })
 })
diff --git a/src/components/GridRow.vue b/src/components/GridRow.vue
index 97a6709..48cee02 100644
--- a/src/components/GridRow.vue
+++ b/src/components/GridRow.vue
@@ -19,6 +19,8 @@ import GridProgressBar from "@/components/GridProgressBar.vue"
 import WorkersDetail from "@/components/WorkersDetail.vue"
 import MiscDetail from "@/components/MiscDetail.vue"
 import SeverityBullet from "@/components/SeverityBullet.vue"
+import IoTooltip from "@/components/tooltip/IoTooltip.vue"
+import TimeTooltip from "@/components/tooltip/TimeTooltip.vue"
 import useNode from "@/node"
 import { directive as vTippy } from "vue-tippy"
 import { HelpService } from "@/services/help-service"
@@ -55,7 +57,6 @@ const {
   formattedProp,
   heapFetchesClass,
   heapFetchesTooltip,
-  ioTooltip,
   localDirtiedPercent,
   localHitPercent,
   localReadPercent,
@@ -74,7 +75,6 @@ const {
   sharedWrittenPercent,
   tempReadPercent,
   tempWrittenPercent,
-  timeTooltip,
   tilde,
 } = useNode(plan, node, viewOptions)
 const showDetails = ref<boolean>(false)
@@ -87,10 +87,14 @@ const showDetails = ref<boolean>(false)
         <span class="font-weight-normal small">#{{ node.nodeId }} </span>
       </a>
     </td>
-    <td
+    <tippy
       class="text-end grid-progress-cell text-nowrap"
+      tag="td"
       v-if="columns.includes('time')"
     >
+      <template #content>
+        <time-tooltip :node="node" />
+      </template>
       <GridProgressBar
         :percentage="
           (node[NodeProp.EXCLUSIVE_DURATION] /
@@ -107,10 +111,7 @@ const showDetails = ref<boolean>(false)
         "
       ></GridProgressBar>
       <!-- time -->
-      <div
-        class="position-relative d-flex"
-        v-tippy="{ content: timeTooltip, allowHTML: true }"
-      >
+      <div class="position-relative d-flex">
         <severity-bullet
           :severity="durationClass"
           v-if="durationClass"
@@ -126,53 +127,65 @@ const showDetails = ref<boolean>(false)
           {{ executionTimePercent }}%
         </template>
       </div>
-    </td>
-    <td
+    </tippy>
+    <tippy
       class="text-end grid-progress-cell text-nowrap"
+      tag="td"
       v-if="columns.includes('ioread')"
-      v-tippy="{ content: ioTooltip, allowHTML: true }"
     >
-      <template v-if="node[NodeProp.IO_READ_TIME]">
+      <template #content>
+        <io-tooltip :node="node" class="mb-0" exclusive />
+      </template>
+      <template v-if="node[NodeProp.EXCLUSIVE_SUM_IO_READ_TIME]">
         <GridProgressBar
           :percentage="
-            (node[NodeProp.EXCLUSIVE_IO_READ_TIME] /
-              (plan.content.Plan[NodeProp.IO_READ_TIME] +
-                plan.content.Plan[NodeProp.IO_WRITE_TIME])) *
+            (node[NodeProp.EXCLUSIVE_SUM_IO_READ_TIME] /
+              (plan.content.Plan[NodeProp.SUM_IO_READ_TIME] +
+                plan.content.Plan[NodeProp.SUM_IO_WRITE_TIME])) *
             100
           "
         ></GridProgressBar>
-        {{ Math.round(node[NodeProp.EXCLUSIVE_IO_READ_TIME]).toLocaleString() }}
+        {{
+          Math.round(node[NodeProp.EXCLUSIVE_SUM_IO_READ_TIME]).toLocaleString()
+        }}
         <div v-if="showDetails" class="small text-body-secondary">
-          {{ duration(node[NodeProp.EXCLUSIVE_IO_READ_TIME]) }}
+          {{ duration(node[NodeProp.EXCLUSIVE_SUM_IO_READ_TIME]) }}
           <br />
-          {{ transferRate(node[NodeProp.AVERAGE_IO_READ_SPEED]) }}
+          {{ transferRate(node[NodeProp.EXCLUSIVE_AVERAGE_SUM_IO_READ_SPEED]) }}
         </div>
       </template>
-    </td>
-    <td
+    </tippy>
+    <tippy
       class="text-end grid-progress-cell text-nowrap"
+      tag="td"
       v-if="columns.includes('iowrite')"
-      v-tippy="{ content: ioTooltip, allowHTML: true }"
     >
-      <template v-if="node[NodeProp.IO_WRITE_TIME]">
+      <template #content>
+        <io-tooltip :node="node" class="mb-0" exclusive />
+      </template>
+      <template v-if="node[NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME]">
         <GridProgressBar
           :percentage="
-            (node[NodeProp.EXCLUSIVE_IO_WRITE_TIME] /
-              (plan.content.Plan[NodeProp.IO_READ_TIME] +
-                plan.content.Plan[NodeProp.IO_WRITE_TIME])) *
+            (node[NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME] /
+              (plan.content.Plan[NodeProp.SUM_IO_READ_TIME] +
+                plan.content.Plan[NodeProp.SUM_IO_WRITE_TIME])) *
             100
           "
         ></GridProgressBar>
         {{
-          Math.round(node[NodeProp.EXCLUSIVE_IO_WRITE_TIME]).toLocaleString()
+          Math.round(
+            node[NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME],
+          ).toLocaleString()
         }}
         <div v-if="showDetails" class="small text-body-secondary">
-          {{ duration(node[NodeProp.EXCLUSIVE_IO_WRITE_TIME]) }}
+          {{ duration(node[NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME]) }}
           <br />
-          {{ transferRate(node[NodeProp.AVERAGE_IO_WRITE_SPEED]) }}
+          {{
+            transferRate(node[NodeProp.EXCLUSIVE_AVERAGE_SUM_IO_WRITE_SPEED])
+          }}
         </div>
       </template>
-    </td>
+    </tippy>
     <td
       class="text-end grid-progress-cell text-nowrap"
       v-if="columns.includes('rows')"
diff --git a/src/components/IoTimingsRow.vue b/src/components/IoTimingsRow.vue
new file mode 100644
index 0000000..7c37d6b
--- /dev/null
+++ b/src/components/IoTimingsRow.vue
@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import { inject, reactive } from "vue"
+import type { Ref } from "vue"
+import * as _ from "lodash"
+import type { IPlan, Node, ViewOptions } from "@/interfaces"
+import { NodeProp, Scope } from "@/enums"
+import { PlanKey, ViewOptionsKey } from "@/symbols"
+import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
+import { directive as vTippy } from "vue-tippy"
+import { HelpService } from "@/services/help-service"
+import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"
+import useNode from "@/node"
+
+interface Props {
+  node: Node
+  scope?: Scope
+  exclusive?: boolean
+}
+const props = withDefaults(defineProps<Props>(), {
+  exclusive: () => false,
+})
+
+const helpService = new HelpService()
+const getHelpMessage = helpService.getHelpMessage
+
+const exclusivePrefix = props.exclusive ? "EXCLUSIVE_" : ""
+
+const node = reactive<Node>(props.node)
+const plan = inject(PlanKey) as Ref<IPlan>
+const viewOptions = inject(ViewOptionsKey) as ViewOptions
+const { formattedProp } = useNode(plan, node, viewOptions)
+const scope = props.scope ? `${props.scope}_`.toUpperCase() : ""
+const name = props.scope ? _.capitalize(props.scope) : "Shared/Local"
+const readTimeProp = (exclusivePrefix +
+  scope +
+  "IO_READ_TIME") as keyof typeof NodeProp
+const readSpeedProp = (exclusivePrefix +
+  "AVERAGE_" +
+  scope +
+  "IO_READ_SPEED") as keyof typeof NodeProp
+const writeTimeProp = (exclusivePrefix +
+  scope +
+  "IO_WRITE_TIME") as keyof typeof NodeProp
+const writeSpeedProp = (exclusivePrefix +
+  "AVERAGE_" +
+  scope +
+  "IO_WRITE_SPEED") as keyof typeof NodeProp
+</script>
+
+<template>
+  <tr>
+    <td>{{ name }}</td>
+    <td class="text-end" v-if="node[NodeProp[readTimeProp]]">
+      {{ formattedProp(readTimeProp) }}
+      <br />
+      <small>~{{ formattedProp(readSpeedProp) }}</small>
+      <FontAwesomeIcon
+        :icon="faInfoCircle"
+        class="cursor-help d-inline-block text-secondary"
+        v-tippy="{
+          content: getHelpMessage('io timings parallel'),
+        }"
+        v-if="
+          node[NodeProp.WORKERS_PLANNED] ||
+          node[NodeProp.WORKERS_PLANNED_BY_GATHER]
+        "
+      ></FontAwesomeIcon>
+    </td>
+    <td class="text-end" v-else>-</td>
+    <td class="text-end" v-if="node[NodeProp[writeTimeProp]]">
+      {{ formattedProp(writeTimeProp) }}
+      <br />
+      <small>~{{ formattedProp(writeSpeedProp) }}</small>
+    </td>
+    <td class="text-end" v-else>-</td>
+  </tr>
+</template>
diff --git a/src/components/Plan.vue b/src/components/Plan.vue
index e11600e..a5ed6ea 100644
--- a/src/components/Plan.vue
+++ b/src/components/Plan.vue
@@ -41,6 +41,7 @@ import { HighlightType, NodeProp } from "@/enums"
 import { json_, pgsql_ } from "@/filters"
 
 import "tippy.js/dist/tippy.css"
+import "tippy.js/themes/light.css"
 import * as d3 from "d3"
 import {
   flextree,
diff --git a/src/components/PlanNodeDetail.vue b/src/components/PlanNodeDetail.vue
index 2686432..bbb5195 100644
--- a/src/components/PlanNodeDetail.vue
+++ b/src/components/PlanNodeDetail.vue
@@ -6,6 +6,7 @@ import type { IPlan, Node, ViewOptions } from "@/interfaces"
 import { HelpService } from "@/services/help-service"
 import { EstimateDirection, NodeProp } from "@/enums"
 import useNode from "@/node"
+import IoTooltip from "@/components/tooltip/IoTooltip.vue"
 import WorkersDetail from "@/components/WorkersDetail.vue"
 import MiscDetail from "@/components/MiscDetail.vue"
 import { PlanKey, ViewOptionsKey } from "@/symbols"
@@ -46,7 +47,6 @@ const activeTab = ref<string>("general")
 
 const helpService = new HelpService()
 const getNodeTypeDescription = helpService.getNodeTypeDescription
-const getHelpMessage = helpService.getHelpMessage
 
 const {
   costClass,
@@ -315,53 +315,20 @@ watch(activeTab, () => {
     </div>
     <div class="tab-pane" :class="{ 'show active': activeTab === 'iobuffer' }">
       <!-- iobuffer tab -->
-      <dl
-        v-if="
-          node[NodeProp.EXCLUSIVE_IO_READ_TIME] ||
-          node[NodeProp.EXCLUSIVE_IO_WRITE_TIME]
-        "
-        class="mb-2 list-inline"
-      >
-        <dt class="list-inline-item align-top">
-          <b> I/O Timings: </b>
-        </dt>
-        <dd class="list-inline-item">
-          <span v-if="node[NodeProp.EXCLUSIVE_IO_READ_TIME]" class="ms-2">
-            <b>Read:&nbsp;</b>
-            {{ formattedProp("EXCLUSIVE_IO_READ_TIME") }}
-            <small>~{{ formattedProp("AVERAGE_IO_READ_SPEED") }}</small>
-            <FontAwesomeIcon
-              :icon="faInfoCircle"
-              class="cursor-help d-inline-block text-secondary"
-              v-tippy="{
-                content: getHelpMessage('io timings parallel'),
-              }"
-              v-if="
-                node[NodeProp.WORKERS_PLANNED] ||
-                node[NodeProp.WORKERS_PLANNED_BY_GATHER]
-              "
-            ></FontAwesomeIcon>
-          </span>
-          <br />
-          <span v-if="node[NodeProp.EXCLUSIVE_IO_WRITE_TIME]" class="ms-2">
-            <b>Write:&nbsp;</b>
-            {{ formattedProp("EXCLUSIVE_IO_WRITE_TIME") }}
-            <small>~{{ formattedProp("AVERAGE_IO_WRITE_SPEED") }}</small>
-          </span>
-        </dd>
-      </dl>
-      <b> Blocks: </b>
+      <io-tooltip :node="node" exclusive />
       <table class="table table-sm">
-        <tbody>
+        <thead>
           <tr>
-            <td></td>
-            <th class="text-end" width="25%">Hit</th>
-            <th class="text-end" width="25%">Read</th>
-            <th class="text-end" width="25%">Dirtied</th>
-            <th class="text-end" width="25%">Written</th>
+            <th>Blocks</th>
+            <td class="text-end" width="25%">Hit</td>
+            <td class="text-end" width="25%">Read</td>
+            <td class="text-end" width="25%">Dirtied</td>
+            <td class="text-end" width="25%">Written</td>
           </tr>
+        </thead>
+        <tbody>
           <tr>
-            <th>Shared</th>
+            <td>Shared</td>
             <td
               class="text-end"
               v-html="formattedProp('EXCLUSIVE_SHARED_HIT_BLOCKS') || '-'"
@@ -380,7 +347,7 @@ watch(activeTab, () => {
             ></td>
           </tr>
           <tr>
-            <th>Temp</th>
+            <td>Temp</td>
             <td class="text-end bg-hatched"></td>
             <td
               class="text-end"
@@ -393,7 +360,7 @@ watch(activeTab, () => {
             ></td>
           </tr>
           <tr>
-            <th>Local</th>
+            <td>Local</td>
             <td
               class="text-end"
               v-html="formattedProp('EXCLUSIVE_LOCAL_HIT_BLOCKS') || '-'"
diff --git a/src/components/PlanStats.vue b/src/components/PlanStats.vue
index 5021155..36386ae 100644
--- a/src/components/PlanStats.vue
+++ b/src/components/PlanStats.vue
@@ -10,6 +10,7 @@ import { directive as vTippy } from "vue-tippy"
 import { NodeProp } from "../enums"
 import { formatNodeProp } from "@/filters"
 import JitDetails from "@/components/JitDetails.vue"
+import IoTooltip from "@/components/tooltip/IoTooltip.vue"
 
 import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"
 import { faCaretDown, faInfoCircle } from "@fortawesome/free-solid-svg-icons"
@@ -20,6 +21,7 @@ const plan = inject(PlanKey) as Ref<IPlan>
 const showSettings = ref<boolean>(false)
 const showTriggers = ref<boolean>(false)
 const showJitDetails = ref<boolean>(false)
+const showIO = ref<boolean>(false)
 const rootNode = computed(() => plan.value && plan.value.content.Plan)
 
 const planningTimeClass = (percent: number) => {
@@ -54,31 +56,17 @@ const triggersTotalDuration = computed(() => {
 })
 
 function averageIO(node: Node) {
-  const read = node[NodeProp.IO_READ_TIME]
-  const read_average = node[NodeProp.AVERAGE_IO_READ_SPEED]
-  const write = node[NodeProp.IO_WRITE_TIME]
-  const write_average = node[NodeProp.AVERAGE_IO_WRITE_SPEED]
+  const readAverage = node[NodeProp.AVERAGE_SUM_IO_READ_SPEED]
+  const writeAverage = node[NodeProp.AVERAGE_SUM_IO_WRITE_SPEED]
   const r = []
-  if (read) {
+  if (readAverage) {
     r.push(
-      `read=${formatNodeProp(
-        NodeProp.IO_READ_TIME,
-        read,
-      )} <small class="text-body-secondary">~${formatNodeProp(
-        NodeProp.AVERAGE_IO_READ_SPEED,
-        read_average,
-      )}</small>`,
+      `read=~${formatNodeProp(NodeProp.AVERAGE_SUM_IO_READ_SPEED, readAverage)}`,
     )
   }
-  if (write) {
+  if (writeAverage) {
     r.push(
-      `write=${formatNodeProp(
-        NodeProp.IO_WRITE_TIME,
-        write,
-      )} <small class="text-body-secondary">~${formatNodeProp(
-        NodeProp.AVERAGE_IO_WRITE_SPEED,
-        write_average,
-      )}</small>`,
+      `write=~${formatNodeProp(NodeProp.AVERAGE_SUM_IO_WRITE_SPEED, writeAverage)}`,
     )
   }
   return r.join(", ")
@@ -279,13 +267,11 @@ function hasParallelChildren(node: Node) {
     </div>
     <div
       class="d-inline-block border-start px-2 position-relative"
-      v-if="
-        rootNode &&
-        (rootNode[NodeProp.AVERAGE_IO_READ_SPEED] ||
-          rootNode[NodeProp.AVERAGE_IO_WRITE_SPEED])
-      "
+      v-if="averageIO(rootNode)"
     >
-      IO: <span v-html="averageIO(rootNode)"></span>
+      <span class="stat-label">
+        IO: <span v-html="averageIO(rootNode)"></span>
+      </span>
       <FontAwesomeIcon
         :icon="faInfoCircle"
         class="cursor-help d-inline-block text-secondary"
@@ -294,6 +280,22 @@ function hasParallelChildren(node: Node) {
         }"
         v-if="hasParallelChildren(rootNode)"
       ></FontAwesomeIcon>
+      <button
+        @click.prevent="showIO = !showIO"
+        class="bg-transparent border-0 p-0 m-0 ps-1"
+      >
+        <FontAwesomeIcon
+          :icon="faCaretDown"
+          class="text-secondary"
+        ></FontAwesomeIcon>
+      </button>
+      <div class="stat-dropdown-container text-start" v-if="showIO">
+        <button
+          class="btn btn-xs btn-close float-end"
+          v-on:click="showIO = false"
+        ></button>
+        <io-tooltip :node="rootNode" class="mb-0" />
+      </div>
     </div>
   </div>
 </template>
diff --git a/src/components/tooltip/IoTooltip.vue b/src/components/tooltip/IoTooltip.vue
new file mode 100644
index 0000000..c3035ac
--- /dev/null
+++ b/src/components/tooltip/IoTooltip.vue
@@ -0,0 +1,141 @@
+<script lang="ts" setup>
+import { computed, reactive } from "vue"
+import type { Node } from "@/interfaces"
+import { NodeProp, Scope } from "@/enums"
+import IoTimingsRow from "@/components/IoTimingsRow.vue"
+
+interface Props {
+  node: Node
+  exclusive?: boolean
+}
+const props = withDefaults(defineProps<Props>(), {
+  exclusive: () => false,
+})
+
+const exclusivePrefix = computed(() => (props.exclusive ? "EXCLUSIVE_" : ""))
+
+const node = reactive<Node>(props.node)
+</script>
+
+<template>
+  <table
+    class="table table-sm"
+    v-if="
+      node[
+        NodeProp[(exclusivePrefix + 'IO_READ_TIME') as keyof typeof NodeProp]
+      ] ||
+      node[
+        NodeProp[(exclusivePrefix + 'IO_WRITE_TIME') as keyof typeof NodeProp]
+      ] ||
+      node[
+        NodeProp[
+          (exclusivePrefix + 'SHARED_IO_READ_TIME') as keyof typeof NodeProp
+        ]
+      ] ||
+      node[
+        NodeProp[
+          (exclusivePrefix + 'SHARED_IO_WRITE_TIME') as keyof typeof NodeProp
+        ]
+      ] ||
+      node[
+        NodeProp[
+          (exclusivePrefix + 'LOCAL_IO_READ_TIME') as keyof typeof NodeProp
+        ]
+      ] ||
+      node[
+        NodeProp[
+          (exclusivePrefix + 'LOCAL_IO_WRITE_TIME') as keyof typeof NodeProp
+        ]
+      ] ||
+      node[
+        NodeProp[
+          (exclusivePrefix + 'TEMP_IO_READ_TIME') as keyof typeof NodeProp
+        ]
+      ] ||
+      node[
+        NodeProp[
+          (exclusivePrefix + 'TEMP_IO_WRITE_TIME') as keyof typeof NodeProp
+        ]
+      ]
+    "
+  >
+    <thead>
+      <tr>
+        <th class="text-nowrap">I/O Timings</th>
+        <td class="text-end" width="50%">Read</td>
+        <td class="text-end" width="50%">Write</td>
+      </tr>
+    </thead>
+    <tbody>
+      <!-- No temp IO: only one line for all IOs is needed -->
+      <io-timings-row
+        :node="node"
+        v-if="
+          node[
+            NodeProp[
+              (exclusivePrefix + 'IO_READ_TIME') as keyof typeof NodeProp
+            ]
+          ] ||
+          node[
+            NodeProp[
+              (exclusivePrefix + 'IO_WRITE_TIME') as keyof typeof NodeProp
+            ]
+          ]
+        "
+        :exclusive="exclusive"
+      />
+      <io-timings-row
+        :node="node"
+        v-if="
+          node[
+            NodeProp[
+              (exclusivePrefix + 'SHARED_IO_READ_TIME') as keyof typeof NodeProp
+            ]
+          ] ||
+          node[
+            NodeProp[
+              (exclusivePrefix +
+                'SHARED_IO_WRITE_TIME') as keyof typeof NodeProp
+            ]
+          ]
+        "
+        :scope="Scope.SHARED"
+        :exclusive="exclusive"
+      />
+      <io-timings-row
+        :node="node"
+        v-if="
+          node[
+            NodeProp[
+              (exclusivePrefix + 'LOCAL_IO_READ_TIME') as keyof typeof NodeProp
+            ]
+          ] ||
+          node[
+            NodeProp[
+              (exclusivePrefix + 'LOCAL_IO_WRITE_TIME') as keyof typeof NodeProp
+            ]
+          ]
+        "
+        :scope="Scope.LOCAL"
+        :exclusive="exclusive"
+      />
+      <io-timings-row
+        :node="node"
+        v-if="
+          node[
+            NodeProp[
+              (exclusivePrefix + 'TEMP_IO_READ_TIME') as keyof typeof NodeProp
+            ]
+          ] ||
+          node[
+            NodeProp[
+              (exclusivePrefix + 'TEMP_IO_WRITE_TIME') as keyof typeof NodeProp
+            ]
+          ]
+        "
+        :scope="Scope.TEMP"
+        :exclusive="exclusive"
+      />
+    </tbody>
+  </table>
+</template>
diff --git a/src/components/tooltip/TimeTooltip.vue b/src/components/tooltip/TimeTooltip.vue
new file mode 100644
index 0000000..f113000
--- /dev/null
+++ b/src/components/tooltip/TimeTooltip.vue
@@ -0,0 +1,15 @@
+<script lang="ts" setup>
+import type { Node } from "@/interfaces"
+import { NodeProp } from "@/enums"
+import { duration } from "@/filters"
+interface Props {
+  node: Node
+}
+defineProps<Props>()
+</script>
+
+<template>
+  Duration:
+  <br />Exclusive: {{ duration(node[NodeProp.EXCLUSIVE_DURATION]) }}, Total:
+  {{ duration(node[NodeProp.ACTUAL_TOTAL_TIME]) }}
+</template>
diff --git a/src/enums.ts b/src/enums.ts
index 3ac4d70..9c849d6 100644
--- a/src/enums.ts
+++ b/src/enums.ts
@@ -87,6 +87,12 @@ export enum NodeProp {
   LOCAL_WRITTEN_BLOCKS = "Local Written Blocks",
   IO_READ_TIME = "I/O Read Time",
   IO_WRITE_TIME = "I/O Write Time",
+  SHARED_IO_READ_TIME = "Shared I/O Read Time",
+  SHARED_IO_WRITE_TIME = "Shared I/O Write Time",
+  LOCAL_IO_READ_TIME = "Local I/O Read Time",
+  LOCAL_IO_WRITE_TIME = "Local I/O Write Time",
+  TEMP_IO_READ_TIME = "Temp I/O Read Time",
+  TEMP_IO_WRITE_TIME = "Temp I/O Write Time",
   OUTPUT = "Output",
   HEAP_FETCHES = "Heap Fetches",
   WAL_RECORDS = "WAL Records",
@@ -124,10 +130,39 @@ export enum NodeProp {
   EXCLUSIVE_LOCAL_DIRTIED_BLOCKS = "*Local Dirtied Blocks (exclusive)",
   EXCLUSIVE_LOCAL_WRITTEN_BLOCKS = "*Local Written Blocks (exclusive)",
 
+  AVERAGE_IO_READ_SPEED = "*I/O Read Speed",
+  AVERAGE_IO_WRITE_SPEED = "*I/O Write Speed",
+  AVERAGE_SHARED_IO_READ_SPEED = "*Shared I/O Read Speed",
+  AVERAGE_SHARED_IO_WRITE_SPEED = "*Shared I/O Write Speed",
+  AVERAGE_LOCAL_IO_READ_SPEED = "*Local I/O Read Speed",
+  AVERAGE_LOCAL_IO_WRITE_SPEED = "*Local I/O Write Speed",
+  AVERAGE_TEMP_IO_READ_SPEED = "*Temp I/O Read Speed",
+  AVERAGE_TEMP_IO_WRITE_SPEED = "*Temp I/O Write Speed",
+
   EXCLUSIVE_IO_READ_TIME = "*I/O Read Time (exclusive)",
   EXCLUSIVE_IO_WRITE_TIME = "*I/O Write Time (exclusive)",
-  AVERAGE_IO_READ_SPEED = "*I/O Read Speed (exclusive)",
-  AVERAGE_IO_WRITE_SPEED = "*I/O Write Speed (exclusive)",
+  EXCLUSIVE_AVERAGE_IO_READ_SPEED = "*I/O Read Speed (exclusive)",
+  EXCLUSIVE_AVERAGE_IO_WRITE_SPEED = "*I/O Write Speed (exclusive)",
+  EXCLUSIVE_SHARED_IO_READ_TIME = "*Shared I/O Read Time (exclusive)",
+  EXCLUSIVE_SHARED_IO_WRITE_TIME = "*Shared I/O Write Time (exclusive)",
+  EXCLUSIVE_AVERAGE_SHARED_IO_READ_SPEED = "*Shared I/O Read Speed (exclusive)",
+  EXCLUSIVE_AVERAGE_SHARED_IO_WRITE_SPEED = "*Shared I/O Write Speed (exclusive)",
+  EXCLUSIVE_LOCAL_IO_READ_TIME = "*Local I/O Read Time (exclusive)",
+  EXCLUSIVE_LOCAL_IO_WRITE_TIME = "*Local I/O Write Time (exclusive)",
+  EXCLUSIVE_AVERAGE_LOCAL_IO_READ_SPEED = "*Local I/O Read Speed (exclusive)",
+  EXCLUSIVE_AVERAGE_LOCAL_IO_WRITE_SPEED = "*Local I/O Write Speed (exclusive)",
+  EXCLUSIVE_TEMP_IO_READ_TIME = "*Temp I/O Read Time (exclusive)",
+  EXCLUSIVE_TEMP_IO_WRITE_TIME = "*Temp I/O Write Time (exclusive)",
+  EXCLUSIVE_AVERAGE_TEMP_IO_READ_SPEED = "*Temp I/O Read Speed (exclusive)",
+  EXCLUSIVE_AVERAGE_TEMP_IO_WRITE_SPEED = "*Temp I/O Write Speed (exclusive)",
+  SUM_IO_READ_TIME = "*I/O Read Time (all scopes)",
+  SUM_IO_WRITE_TIME = "*I/O Write Time (all scopes)",
+  AVERAGE_SUM_IO_READ_SPEED = "*I/O Read Time speed (all scopes)",
+  AVERAGE_SUM_IO_WRITE_SPEED = "*I/O Write Time speed (all scopes)",
+  EXCLUSIVE_SUM_IO_READ_TIME = "*I/O Read Time (exclusive, all scopes)",
+  EXCLUSIVE_SUM_IO_WRITE_TIME = "*I/O Write Time (exclusive, all scopes)",
+  EXCLUSIVE_AVERAGE_SUM_IO_READ_SPEED = "*I/O Read Time speed (exclusive, all scopes)",
+  EXCLUSIVE_AVERAGE_SUM_IO_WRITE_SPEED = "*I/O Write Time speed (exclusive, all scopes)",
 
   WORKERS_PLANNED_BY_GATHER = "*Workers Planned By Gather",
   WORKERS_LAUNCHED_BY_GATHER = "*Workers Launched By Gather",
@@ -197,11 +232,50 @@ nodePropTypes[NodeProp.PLANNER_ESTIMATE_DIRECTION] = PropType.estimateDirection
 
 nodePropTypes[NodeProp.IO_READ_TIME] = PropType.duration
 nodePropTypes[NodeProp.IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.SUM_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.SUM_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.AVERAGE_SUM_IO_READ_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.AVERAGE_SUM_IO_WRITE_SPEED] = PropType.transferRate
 
-nodePropTypes[NodeProp.EXCLUSIVE_IO_READ_TIME] = PropType.duration
-nodePropTypes[NodeProp.EXCLUSIVE_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.IO_WRITE_TIME] = PropType.duration
 nodePropTypes[NodeProp.AVERAGE_IO_READ_SPEED] = PropType.transferRate
 nodePropTypes[NodeProp.AVERAGE_IO_WRITE_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.SHARED_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.SHARED_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.AVERAGE_SHARED_IO_READ_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.AVERAGE_SHARED_IO_WRITE_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.LOCAL_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.LOCAL_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.AVERAGE_LOCAL_IO_READ_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.AVERAGE_LOCAL_IO_WRITE_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.TEMP_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.TEMP_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.AVERAGE_TEMP_IO_READ_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.AVERAGE_TEMP_IO_WRITE_SPEED] = PropType.transferRate
+
+nodePropTypes[NodeProp.EXCLUSIVE_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_IO_READ_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_IO_WRITE_SPEED] = PropType.transferRate
+nodePropTypes[NodeProp.EXCLUSIVE_SHARED_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_SHARED_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_SHARED_IO_READ_SPEED] =
+  PropType.transferRate
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_SHARED_IO_WRITE_SPEED] =
+  PropType.transferRate
+nodePropTypes[NodeProp.EXCLUSIVE_LOCAL_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_LOCAL_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_LOCAL_IO_READ_SPEED] =
+  PropType.transferRate
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_LOCAL_IO_WRITE_SPEED] =
+  PropType.transferRate
+nodePropTypes[NodeProp.EXCLUSIVE_TEMP_IO_READ_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_TEMP_IO_WRITE_TIME] = PropType.duration
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_TEMP_IO_READ_SPEED] =
+  PropType.transferRate
+nodePropTypes[NodeProp.EXCLUSIVE_AVERAGE_TEMP_IO_WRITE_SPEED] =
+  PropType.transferRate
 
 nodePropTypes[NodeProp.EXCLUSIVE_SHARED_HIT_BLOCKS] = PropType.blocks
 nodePropTypes[NodeProp.EXCLUSIVE_SHARED_READ_BLOCKS] = PropType.blocks
@@ -236,3 +310,9 @@ export enum SortSpaceMemoryProp {
   AVERAGE_SORT_SPACE_USED = "Average Sort Space Used",
   PEAK_SORT_SPACE_USED = "Peak Sort Space Used",
 }
+
+export enum Scope {
+  SHARED = "shared",
+  LOCAL = "local",
+  TEMP = "temp",
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 9228d35..6d17b2a 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -110,10 +110,36 @@ export class Node {
   [NodeProp.WORKERS_PLANNED_BY_GATHER]?: number;
   [NodeProp.EXCLUSIVE_IO_READ_TIME]!: number;
   [NodeProp.EXCLUSIVE_IO_WRITE_TIME]!: number;
+  [NodeProp.EXCLUSIVE_SHARED_IO_READ_TIME]!: number;
+  [NodeProp.EXCLUSIVE_SHARED_IO_WRITE_TIME]!: number;
+  [NodeProp.EXCLUSIVE_LOCAL_IO_READ_TIME]!: number;
+  [NodeProp.EXCLUSIVE_LOCAL_IO_WRITE_TIME]!: number;
+  [NodeProp.EXCLUSIVE_TEMP_IO_READ_TIME]!: number;
+  [NodeProp.EXCLUSIVE_TEMP_IO_WRITE_TIME]!: number;
+  [NodeProp.EXCLUSIVE_SUM_IO_READ_TIME]!: number;
+  [NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME]!: number;
   [NodeProp.AVERAGE_IO_READ_SPEED]!: number;
   [NodeProp.AVERAGE_IO_WRITE_SPEED]!: number;
+  [NodeProp.AVERAGE_SHARED_IO_READ_SPEED]!: number;
+  [NodeProp.AVERAGE_SHARED_IO_WRITE_SPEED]!: number;
+  [NodeProp.AVERAGE_LOCAL_IO_READ_SPEED]!: number;
+  [NodeProp.AVERAGE_LOCAL_IO_WRITE_SPEED]!: number;
+  [NodeProp.AVERAGE_TEMP_IO_READ_SPEED]!: number;
+  [NodeProp.AVERAGE_TEMP_IO_WRITE_SPEED]!: number;
+  [NodeProp.AVERAGE_SUM_IO_READ_SPEED]!: number;
+  [NodeProp.AVERAGE_SUM_IO_WRITE_SPEED]!: number;
+  [NodeProp.EXCLUSIVE_AVERAGE_SUM_IO_READ_SPEED]!: number;
+  [NodeProp.EXCLUSIVE_AVERAGE_SUM_IO_WRITE_SPEED]!: number;
   [NodeProp.IO_READ_TIME]!: number;
   [NodeProp.IO_WRITE_TIME]!: number;
+  [NodeProp.SHARED_IO_READ_TIME]!: number;
+  [NodeProp.SHARED_IO_WRITE_TIME]!: number;
+  [NodeProp.LOCAL_IO_READ_TIME]!: number;
+  [NodeProp.LOCAL_IO_WRITE_TIME]!: number;
+  [NodeProp.TEMP_IO_READ_TIME]!: number;
+  [NodeProp.TEMP_IO_WRITE_TIME]!: number;
+  [NodeProp.SUM_IO_READ_TIME]!: number;
+  [NodeProp.SUM_IO_WRITE_TIME]!: number;
   [NodeProp.PARTIAL_MODE]!: string;
   [k: string]:
     | Node[]
diff --git a/src/node.ts b/src/node.ts
index 1f24c8d..bedf466 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -9,15 +9,7 @@ import {
   EstimateDirection,
   HighlightType,
 } from "@/enums"
-import {
-  blocks,
-  cost,
-  duration,
-  factor,
-  formatNodeProp,
-  rows,
-  transferRate,
-} from "@/filters"
+import { blocks, cost, duration, factor, formatNodeProp, rows } from "@/filters"
 import { numberToColorHsl } from "@/services/color-service"
 
 export default function useNode(
@@ -370,15 +362,6 @@ export default function useNode(
     )
   })
 
-  const timeTooltip = computed((): string => {
-    return [
-      "Duration: <br>Exclusive: ",
-      duration(node[NodeProp.EXCLUSIVE_DURATION]),
-      ", Total: ",
-      duration(node[NodeProp.ACTUAL_TOTAL_TIME]),
-    ].join("")
-  })
-
   const rowsTooltip = computed((): string => {
     return ["Rows: ", rows(node[NodeProp.ACTUAL_ROWS_REVISED] as number)].join(
       "",
@@ -461,8 +444,7 @@ export default function useNode(
             written = node[NodeProp.EXCLUSIVE_LOCAL_WRITTEN_BLOCKS]
             break
         }
-        text +=
-          '<table class="table table-dark table-sm table-borderless mb-0">'
+        text += '<table class="table table-sm table-borderless mb-0">'
         text += hit
           ? '<tr><td>Hit:</td><td class="text-end">' +
             blocks(hit, true) +
@@ -505,7 +487,7 @@ export default function useNode(
   )
 
   const buffersByMetricTooltip = computed(() => (metric: NodeProp): string => {
-    let text = '<table class="table table-dark table-sm table-borderless mb-0">'
+    let text = '<table class="table table-sm table-borderless mb-0">'
     text += `<tr><td>${metric}:</td><td class="text-end">`
     if (node[metric]) {
       text += `${blocks(node[metric] as number, true)}</td></tr>`
@@ -513,32 +495,6 @@ export default function useNode(
     return text
   })
 
-  const ioTooltip = computed((): string => {
-    let text = ""
-    const read = node[NodeProp.EXCLUSIVE_IO_READ_TIME]
-    const averageRead = node[NodeProp.AVERAGE_IO_READ_SPEED]
-    const write = node[NodeProp.EXCLUSIVE_IO_WRITE_TIME]
-    const averageWrite = node[NodeProp.AVERAGE_IO_WRITE_SPEED]
-    text += '<table class="table table-dark table-sm table-borderless mb-0">'
-    text += read
-      ? '<tr><td>Read:</td><td class="text-end">' +
-        duration(read) +
-        "<br><small>~" +
-        transferRate(averageRead) +
-        "</small>" +
-        "</td></tr>"
-      : ""
-    text += write
-      ? '<tr><td>Write:</td><td class="text-end">' +
-        duration(write) +
-        "<br><small>~" +
-        transferRate(averageWrite) +
-        "</small>" +
-        "</td></tr>"
-      : ""
-    return "IO " + text
-  })
-
   const heapFetchesTooltip = computed((): string => {
     return `Heap Fetches: ${node[NodeProp.HEAP_FETCHES]?.toLocaleString()}`
   })
@@ -568,7 +524,6 @@ export default function useNode(
     heapFetchesClass,
     heapFetchesTooltip,
     highlightValue,
-    ioTooltip,
     isNeverExecuted,
     isParallelAware,
     localDirtiedPercent,
@@ -592,7 +547,6 @@ export default function useNode(
     tempReadPercent,
     tempWrittenPercent,
     tilde,
-    timeTooltip,
     workersLaunchedCount,
     workersPlannedCount,
     workersPlannedCountReversed,
diff --git a/src/services/help-service.ts b/src/services/help-service.ts
index 2e4543b..790f643 100644
--- a/src/services/help-service.ts
+++ b/src/services/help-service.ts
@@ -340,10 +340,46 @@ const notMiscProperties: string[] = [
   NodeProp.HASH_CONDITION,
   NodeProp.EXCLUSIVE_IO_READ_TIME,
   NodeProp.EXCLUSIVE_IO_WRITE_TIME,
+  NodeProp.EXCLUSIVE_SHARED_IO_READ_TIME,
+  NodeProp.EXCLUSIVE_SHARED_IO_WRITE_TIME,
+  NodeProp.EXCLUSIVE_LOCAL_IO_READ_TIME,
+  NodeProp.EXCLUSIVE_LOCAL_IO_WRITE_TIME,
+  NodeProp.EXCLUSIVE_TEMP_IO_READ_TIME,
+  NodeProp.EXCLUSIVE_TEMP_IO_WRITE_TIME,
+  NodeProp.EXCLUSIVE_AVERAGE_IO_READ_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_IO_WRITE_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_SHARED_IO_READ_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_SHARED_IO_WRITE_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_LOCAL_IO_READ_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_LOCAL_IO_WRITE_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_TEMP_IO_READ_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_TEMP_IO_WRITE_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_IO_READ_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_IO_WRITE_SPEED,
+  NodeProp.EXCLUSIVE_SUM_IO_READ_TIME,
+  NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME,
+  NodeProp.EXCLUSIVE_AVERAGE_SUM_IO_READ_SPEED,
+  NodeProp.EXCLUSIVE_AVERAGE_SUM_IO_WRITE_SPEED,
   NodeProp.AVERAGE_IO_READ_SPEED,
   NodeProp.AVERAGE_IO_WRITE_SPEED,
-  NodeProp.IO_READ_TIME, // Exclusive value already shown in IO tab
-  NodeProp.IO_WRITE_TIME, // Exclusive value already shown in IO tab
+  NodeProp.AVERAGE_SHARED_IO_READ_SPEED,
+  NodeProp.AVERAGE_SHARED_IO_WRITE_SPEED,
+  NodeProp.AVERAGE_LOCAL_IO_READ_SPEED,
+  NodeProp.AVERAGE_LOCAL_IO_WRITE_SPEED,
+  NodeProp.AVERAGE_TEMP_IO_READ_SPEED,
+  NodeProp.AVERAGE_TEMP_IO_WRITE_SPEED,
+  NodeProp.IO_READ_TIME,
+  NodeProp.IO_WRITE_TIME,
+  NodeProp.SHARED_IO_READ_TIME,
+  NodeProp.SHARED_IO_WRITE_TIME,
+  NodeProp.LOCAL_IO_READ_TIME,
+  NodeProp.LOCAL_IO_WRITE_TIME,
+  NodeProp.TEMP_IO_READ_TIME,
+  NodeProp.TEMP_IO_WRITE_TIME,
+  NodeProp.SUM_IO_READ_TIME,
+  NodeProp.SUM_IO_WRITE_TIME,
+  NodeProp.AVERAGE_SUM_IO_READ_SPEED,
+  NodeProp.AVERAGE_SUM_IO_WRITE_SPEED,
   NodeProp.HEAP_FETCHES,
   NodeProp.WAL_RECORDS,
   NodeProp.WAL_BYTES,
diff --git a/src/services/plan-service.ts b/src/services/plan-service.ts
index 42dfb5f..a9bc16d 100644
--- a/src/services/plan-service.ts
+++ b/src/services/plan-service.ts
@@ -191,8 +191,8 @@ export class PlanService {
     }
     function sumIo(o: Node) {
       return (
-        (o[NodeProp.EXCLUSIVE_IO_READ_TIME] as number) +
-        (o[NodeProp.EXCLUSIVE_IO_WRITE_TIME] as number)
+        (o[NodeProp.EXCLUSIVE_SUM_IO_READ_TIME] as number) +
+        (o[NodeProp.EXCLUSIVE_SUM_IO_WRITE_TIME] as number)
       )
     }
     const highestIo = _.maxBy(flat, (o) => {
@@ -1026,29 +1026,90 @@ export class PlanService {
     const iotimingsRegex = /I\/O Timings:\s+(.*)\s*$/g
     const iotimingsMatches = iotimingsRegex.exec(text)
 
-    /*
-     * Groups:
-     * 1: type
-     * 2: info
-     */
-    if (iotimingsMatches) {
-      // Initiate with default value
+    if (!iotimingsMatches) {
+      return false
+    }
+
+    const scopeRegex =
+      /\b(shared\/local|shared|local|temp)((?:\s+(?:read|write)=\d+(?:\.\d+)?)+)/g
+    const operationRegex = /(read|write)=(\d+(?:\.\d+)?)/g
+
+    const results = []
+    let scopeMatch
+    let operationMatch
+
+    // 1. Handle scoped timings like "local read=10 write=20"
+    while ((scopeMatch = scopeRegex.exec(text)) !== null) {
+      const scope = scopeMatch[1]
+      const operationsString = scopeMatch[2]
+
+      const entry = { scope, read: 0, write: 0 }
+
+      while (
+        (operationMatch = operationRegex.exec(operationsString)) !== null
+      ) {
+        entry[operationMatch[1] as "read" | "write"] = parseFloat(
+          operationMatch[2],
+        )
+      }
+
+      results.push(entry)
+    }
+
+    // 2. Handle unscoped timings like "read=0.011" outside of scoped blocks
+    const rest = text.replace(scopeRegex, "")
+    const unscoped = { scope: undefined, read: 0, write: 0 }
+    let found = false
+
+    while ((operationMatch = operationRegex.exec(rest)) !== null) {
+      unscoped[operationMatch[1] as "read" | "write"] = parseFloat(
+        operationMatch[2],
+      )
+      found = true
+    }
+
+    if (found) {
+      results.push(unscoped)
+    }
+
+    const scopeIsDetailed = _.some(results, (result) => {
+      return result.scope == "shared" || result.scope == "local"
+    })
+
+    const scopeIsPartiallyDetailed = _.some(results, (result) => {
+      return result.scope == "shared/local"
+    })
+
+    // Initiate with default value
+    if (scopeIsDetailed) {
+      el[NodeProp.SHARED_IO_READ_TIME] = 0
+      el[NodeProp.SHARED_IO_WRITE_TIME] = 0
+      el[NodeProp.LOCAL_IO_READ_TIME] = 0
+      el[NodeProp.LOCAL_IO_WRITE_TIME] = 0
+    } else {
       el[NodeProp.IO_READ_TIME] = 0
       el[NodeProp.IO_WRITE_TIME] = 0
+    }
+    if (scopeIsPartiallyDetailed || scopeIsDetailed) {
+      el[NodeProp.TEMP_IO_READ_TIME] = 0
+      el[NodeProp.TEMP_IO_WRITE_TIME] = 0
+    }
 
-      _.each(iotimingsMatches[1].split(/\s+/), (timing) => {
-        const s = timing.split(/=/)
-        const method = s[0]
-        const value = parseFloat(s[1])
-        const prop = ("IO_" +
-          _.upperCase(method) +
-          "_TIME") as keyof typeof NodeProp
+    results.forEach((result) => {
+      ;["read", "write"].forEach((operation) => {
+        let prop = `IO_${_.upperCase(operation)}_TIME` as keyof typeof NodeProp
+
+        if (result.scope && result.scope != "shared/local") {
+          prop = (_.upperCase(result.scope) +
+            "_" +
+            prop) as keyof typeof NodeProp
+        }
         const nodeProp = NodeProp[prop] as unknown as keyof typeof Node
-        el[nodeProp] = value
+        el[nodeProp] = result[operation as "read" | "write"]
       })
-      return true
-    }
-    return false
+    })
+
+    return true
   }
 
   private parseOptions(text: string, el: Node): boolean {
@@ -1180,17 +1241,27 @@ export class PlanService {
       "LOCAL_WRITTEN_BLOCKS",
       "IO_READ_TIME",
       "IO_WRITE_TIME",
+      "SHARED_IO_READ_TIME",
+      "SHARED_IO_WRITE_TIME",
+      "LOCAL_IO_READ_TIME",
+      "LOCAL_IO_WRITE_TIME",
+      "TEMP_IO_READ_TIME",
+      "TEMP_IO_WRITE_TIME",
     ]
     _.each(properties, (property) => {
-      const sum = _.sumBy(node[NodeProp.PLANS], (child: Node) => {
-        return (child[NodeProp[property]] as number) || 0
-      })
+      const sum = Number(
+        _.sumBy(node[NodeProp.PLANS], (child: Node) => {
+          return (child[NodeProp[property]] as number) || 0
+        }).toFixed(3),
+      )
       const exclusivePropertyString = ("EXCLUSIVE_" +
         property) as keyof typeof NodeProp
       const nodeProp = NodeProp[
         exclusivePropertyString
       ] as unknown as keyof typeof Node
-      node[nodeProp] = (node[NodeProp[property]] as number) - sum
+      node[nodeProp] = Number(
+        ((node[NodeProp[property]] as number) - sum).toFixed(3),
+      )
     })
   }
 
@@ -1204,17 +1275,83 @@ export class PlanService {
       node[NodeProp["AVERAGE_IO_READ_SPEED"]] =
         (sharedReadBlocks + localReadBlocks) / (ioReadTime / 1000)
     }
-
-    const ioWriteTime =
-      (node[NodeProp["EXCLUSIVE_IO_WRITE_TIME"]] as number) || 0
-    if (ioWriteTime) {
-      const sharedWriteBlocks =
-        (node[NodeProp["EXCLUSIVE_SHARED_WRITTEN_BLOCKS"]] as number) || 0
-      const localWriteBlocks =
-        (node[NodeProp["EXCLUSIVE_LOCAL_WRITTEN_BLOCKS"]] as number) || 0
-      node[NodeProp["AVERAGE_IO_WRITE_SPEED"]] =
-        (sharedWriteBlocks + localWriteBlocks) / (ioWriteTime / 1000)
+    // The matrix to match I/O Timings with Buffers
+    let scopesMatrix
+    if (_.isUndefined(node[NodeProp.TEMP_IO_READ_TIME])) {
+      // pre Pg15
+      scopesMatrix = {
+        "": ["shared", "local"],
+      }
+    } else if (!_.isUndefined(node[NodeProp.IO_READ_TIME])) {
+      // pg15-16
+      scopesMatrix = {
+        "": ["shared", "local"],
+        temp: ["temp"],
+      }
+    } else {
+      // pg17+
+      scopesMatrix = {
+        shared: ["shared"],
+        local: ["local"],
+        temp: ["temp"],
+      }
     }
+    const operations = ["read", "write"]
+    const buffersOperations = ["read", "written"]
+
+    _.forEach(scopesMatrix, (buffersScopes, timingScope) => {
+      operations.forEach((operation, index) => {
+        ;["exclusive_", ""].forEach((prefix) => {
+          const timeProp =
+            `${prefix}${timingScope ? timingScope + "_" : ""}io_${operation}_time`.toUpperCase() as keyof typeof NodeProp
+          const speedProp =
+            `${prefix}average_${timingScope ? timingScope + "_" : ""}io_${operation}_speed`.toUpperCase() as keyof typeof NodeProp
+          const time = (node[NodeProp[timeProp]] as number) || 0
+          const buffersOperation = buffersOperations[index]
+          const buffers = _.sumBy(buffersScopes, (bufferScope) => {
+            const bufferProp =
+              `${prefix}${bufferScope}_${buffersOperation}_blocks`.toUpperCase() as keyof typeof NodeProp
+            return (node[NodeProp[bufferProp]] as number) || 0
+          })
+          if (time) {
+            node[NodeProp[speedProp] as unknown as keyof typeof Node] = Number(
+              (buffers / (time / 1000)).toFixed(3),
+            )
+          }
+        })
+      })
+    })
+
+    // We also compute sum and average speed for read / write timings for all scopes
+    operations.forEach((operation, index) => {
+      ;["exclusive_", ""].forEach((prefix) => {
+        const sumTimeProp =
+          `${prefix}sum_io_${operation}_time`.toUpperCase() as keyof typeof NodeProp
+        const speedProp =
+          `${prefix}average_sum_io_${operation}_speed`.toUpperCase() as keyof typeof NodeProp
+        let time = 0
+        let buffers = 0
+        _.forEach(scopesMatrix, (buffersScopes, timingScope) => {
+          const timeProp =
+            `${prefix}${timingScope ? timingScope + "_" : ""}io_${operation}_time`.toUpperCase() as keyof typeof NodeProp
+          time += (node[NodeProp[timeProp]] as number) || 0
+          const buffersOperation = buffersOperations[index]
+          buffers += _.sumBy(buffersScopes, (bufferScope) => {
+            const bufferProp =
+              `${prefix}${bufferScope}_${buffersOperation}_blocks`.toUpperCase() as keyof typeof NodeProp
+            return (node[NodeProp[bufferProp]] as number) || 0
+          })
+        })
+        node[NodeProp[sumTimeProp] as unknown as keyof typeof Node] = Number(
+          time.toFixed(3),
+        )
+        if (time) {
+          node[NodeProp[speedProp] as unknown as keyof typeof Node] = Number(
+            (buffers / (time / 1000)).toFixed(3),
+          )
+        }
+      })
+    })
   }
 
   private findOutputProperty(node: Node): boolean {


Test patch:
diff --git a/src/services/__tests__/21-io-timings.spec.ts b/src/services/__tests__/21-io-timings.spec.ts
new file mode 100644
index 0000000..35ae52a
--- /dev/null
+++ b/src/services/__tests__/21-io-timings.spec.ts
@@ -0,0 +1,28 @@
+import { describe, expect, test } from "vitest"
+import type { IPlan, IPlanContent } from "@/interfaces"
+import { PlanService } from "@/services/plan-service"
+import _ from "lodash"
+import * as fs from "fs"
+import { fileURLToPath } from "url"
+import * as path from "path"
+
+const __filename = fileURLToPath(import.meta.url)
+const dir = path.join(path.dirname(__filename), "21-io-timings")
+const files = fs.readdirSync(dir)
+let tests = files.map((file: string) => file.replace(/\..*/, ""))
+tests = _.uniq(tests)
+tests.forEach((planTest: string) => {
+  describe("From text: Plan " + planTest, () => {
+    test("", () => {
+      const planFile = path.join(dir, planTest + ".plan")
+      const planText = fs.readFileSync(planFile, { encoding: "utf-8" })
+      const planExpectFile = path.join(dir, planTest + ".expect")
+      const planExpect = fs.readFileSync(planExpectFile, { encoding: "utf-8" })
+      const planService = new PlanService()
+      const r = planService.fromSource(planText) as IPlanContent
+      const plan: IPlan = planService.createPlan("", r, "")
+      // Verify timings for the first node
+      expect(plan.content.Plan).toMatchObject(JSON.parse(planExpect))
+    })
+  })
+})
diff --git a/src/services/__tests__/21-io-timings/01-text.expect b/src/services/__tests__/21-io-timings/01-text.expect
new file mode 100644
index 0000000..06e5331
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/01-text.expect
@@ -0,0 +1,6 @@
+{
+  "I/O Read Time": 0.011,
+  "I/O Write Time": 0,
+  "*I/O Read Time (all scopes)": 0.011,
+  "*I/O Write Time (all scopes)": 0
+}
diff --git a/src/services/__tests__/21-io-timings/01-text.plan b/src/services/__tests__/21-io-timings/01-text.plan
new file mode 100644
index 0000000..d6b7895
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/01-text.plan
@@ -0,0 +1,9 @@
+ Seq Scan on public.actor  (cost=0.00..4.00 rows=200 width=25) (actual time=0.010..0.030 rows=200 loops=1)
+   Output: actor_id, first_name, last_name, last_update
+   Buffers: shared read=2
+   I/O Timings: read=0.011
+ Planning:
+   Buffers: shared hit=39 read=7
+   I/O Timings: read=0.049
+ Planning Time: 0.223 ms
+ Execution Time: 0.049 ms
diff --git a/src/services/__tests__/21-io-timings/02-text.expect b/src/services/__tests__/21-io-timings/02-text.expect
new file mode 100644
index 0000000..8c848ba
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/02-text.expect
@@ -0,0 +1,7 @@
+{
+  "Shared I/O Write Time": 23.77,
+  "Local I/O Write Time": 25.997,
+  "Temp I/O Read Time": 324,
+  "*I/O Read Time (all scopes)": 324,
+  "*I/O Write Time (all scopes)": 49.767
+}
diff --git a/src/services/__tests__/21-io-timings/02-text.plan b/src/services/__tests__/21-io-timings/02-text.plan
new file mode 100644
index 0000000..f60c394
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/02-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: local hit=1008849 dirtied=4425 written=8272
+   I/O Timings: shared write=23.77, local write=25.997, temp read=324
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/03-text.expect b/src/services/__tests__/21-io-timings/03-text.expect
new file mode 100644
index 0000000..f7c2240
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/03-text.expect
@@ -0,0 +1,6 @@
+{
+  "I/O Read Time": 16.962,
+  "I/O Write Time": 0.018,
+  "*I/O Read Time (all scopes)": 16.962,
+  "*I/O Write Time (all scopes)": 0.018
+}
diff --git a/src/services/__tests__/21-io-timings/03-text.plan b/src/services/__tests__/21-io-timings/03-text.plan
new file mode 100644
index 0000000..3f021bc
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/03-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: shared hit=234 read=8029 written=1
+   I/O Timings: shared/local read=16.962 write=0.018
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/04-text.expect b/src/services/__tests__/21-io-timings/04-text.expect
new file mode 100644
index 0000000..c7debef
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/04-text.expect
@@ -0,0 +1,10 @@
+{
+  "Shared I/O Read Time": 0,
+  "Shared I/O Write Time": 0,
+  "Local I/O Read Time": 0,
+  "Local I/O Write Time": 25.997,
+  "Temp I/O Read Time": 0,
+  "Temp I/O Write Time": 0,
+  "*I/O Read Time (all scopes)": 0,
+  "*I/O Write Time (all scopes)": 25.997
+}
diff --git a/src/services/__tests__/21-io-timings/04-text.plan b/src/services/__tests__/21-io-timings/04-text.plan
new file mode 100644
index 0000000..0ff205a
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/04-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: local hit=1008849 dirtied=4425 written=8272
+   I/O Timings: local write=25.997
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/05-text.expect b/src/services/__tests__/21-io-timings/05-text.expect
new file mode 100644
index 0000000..18c8007
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/05-text.expect
@@ -0,0 +1,4 @@
+{
+  "Temp I/O Write Time": 16022.349,
+  "*I/O Write Time (all scopes)": 16022.349
+}
diff --git a/src/services/__tests__/21-io-timings/05-text.plan b/src/services/__tests__/21-io-timings/05-text.plan
new file mode 100644
index 0000000..fd4a21c
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/05-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: shared hit=5063706, temp written=864226
+   I/O Timings: temp write=16022.349
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/06-text.expect b/src/services/__tests__/21-io-timings/06-text.expect
new file mode 100644
index 0000000..ced6c28
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/06-text.expect
@@ -0,0 +1,8 @@
+{
+  "I/O Read Time": 161740.119,
+  "I/O Write Time": 829.482,
+  "Temp I/O Read Time": 2842.295,
+  "Temp I/O Write Time": 6913.904,
+  "*I/O Read Time (all scopes)": 164582.414,
+  "*I/O Write Time (all scopes)": 7743.386
+}
diff --git a/src/services/__tests__/21-io-timings/06-text.plan b/src/services/__tests__/21-io-timings/06-text.plan
new file mode 100644
index 0000000..678c11b
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/06-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: shared hit=12 read=650754 dirtied=134607 written=95274, temp read=375311 written=375311
+   I/O Timings: shared/local read=161740.119 write=829.482, temp read=2842.295 write=6913.904
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/07-text.expect b/src/services/__tests__/21-io-timings/07-text.expect
new file mode 100644
index 0000000..bd57c66
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/07-text.expect
@@ -0,0 +1,6 @@
+{
+  "I/O Read Time": 0,
+  "I/O Write Time": 23.77,
+  "*I/O Read Time (all scopes)": 0,
+  "*I/O Write Time (all scopes)": 23.77
+}
diff --git a/src/services/__tests__/21-io-timings/07-text.plan b/src/services/__tests__/21-io-timings/07-text.plan
new file mode 100644
index 0000000..487abd6
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/07-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: local hit=1008849 dirtied=4425 written=8272
+   I/O Timings: shared/local write=23.77
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/08-text.expect b/src/services/__tests__/21-io-timings/08-text.expect
new file mode 100644
index 0000000..ca386e8
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/08-text.expect
@@ -0,0 +1,8 @@
+{
+  "I/O Read Time": 0,
+  "I/O Write Time": 0,
+  "Temp I/O Read Time": 2.049,
+  "Temp I/O Write Time": 54.951,
+  "*I/O Read Time (all scopes)": 2.049,
+  "*I/O Write Time (all scopes)": 54.951
+}
diff --git a/src/services/__tests__/21-io-timings/08-text.plan b/src/services/__tests__/21-io-timings/08-text.plan
new file mode 100644
index 0000000..973790d
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/08-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: shared hit=710385, temp read=784 written=787
+   I/O Timings: temp read=2.049 write=54.951
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/09-text.expect b/src/services/__tests__/21-io-timings/09-text.expect
new file mode 100644
index 0000000..c4fa832
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/09-text.expect
@@ -0,0 +1,10 @@
+{
+  "Shared I/O Read Time": 0,
+  "Shared I/O Write Time": 23.77,
+  "Local I/O Read Time": 0,
+  "Local I/O Write Time": 0,
+  "Temp I/O Read Time": 0,
+  "Temp I/O Write Time": 0,
+  "*I/O Read Time (all scopes)": 0,
+  "*I/O Write Time (all scopes)": 23.77
+}
diff --git a/src/services/__tests__/21-io-timings/09-text.plan b/src/services/__tests__/21-io-timings/09-text.plan
new file mode 100644
index 0000000..494e924
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/09-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: local hit=1008849 dirtied=4425 written=8272
+   I/O Timings: shared write=23.77
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/10-text.expect b/src/services/__tests__/21-io-timings/10-text.expect
new file mode 100644
index 0000000..aa553ee
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/10-text.expect
@@ -0,0 +1,6 @@
+{
+  "I/O Read Time": 0.020,
+  "I/O Write Time": 0,
+  "*I/O Read Time (all scopes)": 0.020,
+  "*I/O Write Time (all scopes)": 0
+}
diff --git a/src/services/__tests__/21-io-timings/10-text.plan b/src/services/__tests__/21-io-timings/10-text.plan
new file mode 100644
index 0000000..211410e
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/10-text.plan
@@ -0,0 +1,10 @@
+                                                       QUERY PLAN
+────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ Index Scan using idea_1 on domains  (cost=0.56..8.59 rows=3407 width=60) (actual time=0.049..23.552 rows=8183 loops=1)
+   Index Cond: ((domain ~>=~ 'hu'::text) AND (domain ~<~ 'hv'::text))
+   Filter: (domain ~~ 'hu%'::text)
+   Buffers: shared hit=7, local read=2
+   I/O Timings: shared/local read=0.020
+ Planning Time: 0.073 ms
+ Execution Time: 23.826 ms
+(7 rows)
diff --git a/src/services/__tests__/21-io-timings/read-and-write.expect b/src/services/__tests__/21-io-timings/read-and-write.expect
new file mode 100644
index 0000000..66a0505
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/read-and-write.expect
@@ -0,0 +1,8 @@
+{
+  "I/O Read Time": 77240.327,
+  "I/O Write Time": 222.281,
+  "*I/O Read Time (exclusive, all scopes)": 0,
+  "*I/O Write Time (exclusive, all scopes)": 0,
+  "*I/O Read Time (all scopes)": 77240.327,
+  "*I/O Write Time (all scopes)": 222.281
+}
diff --git a/src/services/__tests__/21-io-timings/read-and-write.plan b/src/services/__tests__/21-io-timings/read-and-write.plan
new file mode 100644
index 0000000..5131b48
--- /dev/null
+++ b/src/services/__tests__/21-io-timings/read-and-write.plan
@@ -0,0 +1,28 @@
+ Sort  (cost=662411.02..663385.77 rows=389900 width=12) (actual time=79447.528..79480.788 rows=415874 loops=1)
+   Sort Key: s.max_strength DESC NULLS LAST, p.id
+   Sort Method: quicksort  Memory: 31783kB
+   Buffers: shared hit=323557 read=232912 written=10900
+   I/O Timings: read=77240.327 write=222.281
+   ->  Hash Left Join  (cost=2688.36..626203.46 rows=389900 width=12) (actual time=73602.258..79139.147 rows=415874 loops=1)
+         Hash Cond: ((p.org_id = s.org_id) AND (p.id = s.external_id))
+         Buffers: shared hit=323551 read=232912 written=10900
+         I/O Timings: read=77240.327 write=222.281
+         ->  Index Scan using persons_pkey on persons p  (cost=0.57..621468.69 rows=389900 width=8) (actual time=0.017..5331.909 rows=415874 loops=1)
+               Index Cond: (org_id = 7)
+               Filter: (type = 0)
+               Rows Removed by Filter: 347
+               Buffers: shared hit=323458 read=82125 written=4694
+               I/O Timings: read=4545.702 write=86.673
+         ->  Hash  (cost=2644.17..2644.17 rows=2908 width=16) (actual time=73602.213..73602.214 rows=149755 loops=1)
+               Buckets: 262144 (originally 4096)  Batches: 1 (originally 1)  Memory Usage: 9068kB
+               Buffers: shared hit=93 read=150787 written=6206
+               I/O Timings: read=72694.625 write=135.607
+               ->  Index Scan using persons_max_rel_strengths_org_id_internal_id_external_id_index on persons_max_rel_strengths s  (cost=0.58..2644.17 rows=2908 width=16) (actual time=4.677..73498.577 rows=149755 loops=1)
+                     Index Cond: ((org_id = 7) AND (internal_id = 68233836))
+                     Buffers: shared hit=93 read=150787 written=6206
+                     I/O Timings: read=72694.625 write=135.607
+ Planning:
+   Buffers: shared hit=540 read=11 written=3
+   I/O Timings: read=0.059 write=0.366
+ Planning Time: 3.115 ms
+ Execution Time: 79515.870 ms