diff --git a/gcc/diagnostic-client-data-hooks.h b/gcc/diagnostic-client-data-hooks.h
index 5f8b9a25294..e3f2d056207 100644
--- a/gcc/diagnostic-client-data-hooks.h
+++ b/gcc/diagnostic-client-data-hooks.h
@@ -21,6 +21,7 @@ along with GCC; see the file COPYING3.  If not see
 #ifndef GCC_DIAGNOSTIC_CLIENT_DATA_HOOKS_H
 #define GCC_DIAGNOSTIC_CLIENT_DATA_HOOKS_H
 
+class sarif_object;
 class client_version_info;
 
 /* A bundle of additional metadata, owned by the diagnostic_context,
@@ -41,6 +42,11 @@ class diagnostic_client_data_hooks
      See SARIF v2.1.0 Appendix J for suggested values.  */
   virtual const char *
   maybe_get_sarif_source_language (const char *filename) const = 0;
+
+  /* Hook to allow client to populate a SARIF "invocation" object with
+     a custom property bag (see SARIF v2.1.0 section 3.8).  */
+  virtual void
+  add_sarif_invocation_properties (sarif_object &invocation_obj) const = 0;
 };
 
 /* Factory function for making an instance of diagnostic_client_data_hooks
diff --git a/gcc/diagnostic-format-sarif.cc b/gcc/diagnostic-format-sarif.cc
index 5e483988027..1eff71962d7 100644
--- a/gcc/diagnostic-format-sarif.cc
+++ b/gcc/diagnostic-format-sarif.cc
@@ -32,13 +32,14 @@ along with GCC; see the file COPYING3.  If not see
 #include "diagnostic-client-data-hooks.h"
 #include "diagnostic-diagram.h"
 #include "text-art/canvas.h"
+#include "diagnostic-format-sarif.h"
 
 class sarif_builder;
 
 /* Subclass of json::object for SARIF invocation objects
    (SARIF v2.1.0 section 3.20).  */
 
-class sarif_invocation : public json::object
+class sarif_invocation : public sarif_object
 {
 public:
   sarif_invocation ()
@@ -49,17 +50,17 @@ public:
   void add_notification_for_ice (diagnostic_context *context,
 				 diagnostic_info *diagnostic,
 				 sarif_builder *builder);
-  void prepare_to_flush ();
+  void prepare_to_flush (diagnostic_context *context);
 
 private:
   json::array *m_notifications_arr;
   bool m_success;
 };
 
-/* Subclass of json::object for SARIF result objects
+/* Subclass of sarif_object for SARIF result objects
    (SARIF v2.1.0 section 3.27).  */
 
-class sarif_result : public json::object
+class sarif_result : public sarif_object
 {
 public:
   sarif_result () : m_related_locations_arr (NULL) {}
@@ -79,13 +80,13 @@ private:
   json::array *m_related_locations_arr;
 };
 
-/* Subclass of json::object for SARIF notification objects
+/* Subclass of sarif_object for SARIF notification objects
    (SARIF v2.1.0 section 3.58).
 
    This subclass is specifically for notifying when an
    internal compiler error occurs.  */
 
-class sarif_ice_notification : public json::object
+class sarif_ice_notification : public sarif_object
 {
 public:
   sarif_ice_notification (diagnostic_context *context,
@@ -232,7 +233,24 @@ private:
 
 static sarif_builder *the_builder;
 
-/* class sarif_invocation : public json::object.  */
+/* class sarif_object : public json::object.  */
+
+sarif_property_bag &
+sarif_object::get_or_create_properties ()
+{
+  json::value *properties_val = get ("properties");
+  if (properties_val)
+    {
+      if (properties_val->get_kind () == json::JSON_OBJECT)
+	return *static_cast <sarif_property_bag *> (properties_val);
+    }
+
+  sarif_property_bag *bag = new sarif_property_bag ();
+  set ("properties", bag);
+  return *bag;
+}
+
+/* class sarif_invocation : public sarif_object.  */
 
 /* Handle an internal compiler error DIAGNOSTIC occurring on CONTEXT.
    Add an object representing the ICE to the notifications array.  */
@@ -250,16 +268,21 @@ sarif_invocation::add_notification_for_ice (diagnostic_context *context,
 }
 
 void
-sarif_invocation::prepare_to_flush ()
+sarif_invocation::prepare_to_flush (diagnostic_context *context)
 {
   /* "executionSuccessful" property (SARIF v2.1.0 section 3.20.14).  */
   set ("executionSuccessful", new json::literal (m_success));
 
   /* "toolExecutionNotifications" property (SARIF v2.1.0 section 3.20.21).  */
   set ("toolExecutionNotifications", m_notifications_arr);
+
+  /* Call client hook, allowing it to create a custom property bag for
+     this object (SARIF v2.1.0 section 3.8) e.g. for recording time vars.  */
+  if (context->m_client_data_hooks)
+    context->m_client_data_hooks->add_sarif_invocation_properties (*this);
 }
 
-/* class sarif_result : public json::object.  */
+/* class sarif_result : public sarif_object.  */
 
 /* Handle secondary diagnostics that occur within a diagnostic group.
    The closest SARIF seems to have to nested diagnostics is the
@@ -319,7 +342,7 @@ sarif_result::add_related_location (json::object *location_obj)
   m_related_locations_arr->append (location_obj);
 }
 
-/* class sarif_ice_notification : public json::object.  */
+/* class sarif_ice_notification : public sarif_object.  */
 
 /* sarif_ice_notification's ctor.
    DIAGNOSTIC is an internal compiler error.  */
@@ -415,7 +438,7 @@ sarif_builder::end_group ()
 void
 sarif_builder::flush_to_file (FILE *outf)
 {
-  m_invocation_obj->prepare_to_flush ();
+  m_invocation_obj->prepare_to_flush (m_context);
   json::object *top = make_top_level_object (m_invocation_obj, m_results_array);
   top->dump (outf);
   m_invocation_obj = NULL;
diff --git a/gcc/diagnostic-format-sarif.h b/gcc/diagnostic-format-sarif.h
new file mode 100644
index 00000000000..82ed9b9ee44
--- /dev/null
+++ b/gcc/diagnostic-format-sarif.h
@@ -0,0 +1,45 @@
+/* SARIF output for diagnostics.
+   Copyright (C) 2023 Free Software Foundation, Inc.
+   Contributed by David Malcolm <dmalcolm@redhat.com>.
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3.  If not see
+<http://www.gnu.org/licenses/>.  */
+
+#ifndef GCC_DIAGNOSTIC_FORMAT_SARIF_H
+#define GCC_DIAGNOSTIC_FORMAT_SARIF_H
+
+#include "json.h"
+
+/* Concrete subclass of json::object for SARIF property bags
+   (SARIF v2.1.0 section 3.8).  */
+
+class sarif_property_bag : public json::object
+{
+};
+
+/* Concrete subclass of json::object for SARIF objects that can
+   contain property bags (as per SARIF v2.1.0 section 3.8.1, which has:
+   "In addition to those properties that are explicitly documented, every
+   object defined in this document MAY contain a property named properties
+   whose value is a property bag.")  */
+
+class sarif_object : public json::object
+{
+public:
+  sarif_property_bag &get_or_create_properties ();
+};
+
+#endif /* ! GCC_DIAGNOSTIC_FORMAT_SARIF_H */
diff --git a/gcc/doc/invoke.texi b/gcc/doc/invoke.texi
index fa765d5a0dd..5ea45c6566d 100644
--- a/gcc/doc/invoke.texi
+++ b/gcc/doc/invoke.texi
@@ -19932,8 +19932,16 @@ print some statistics about each pass when it finishes.
 
 @opindex ftime-report
 @item -ftime-report
-Makes the compiler print some statistics about the time consumed by each
-pass when it finishes.
+Makes the compiler print some statistics to stderr about the time consumed
+by each pass when it finishes.
+
+If SARIF output of diagnostics was requested via
+@option{-fdiagnostics-format=sarif-file} or
+@option{-fdiagnostics-format=sarif-stderr} then the @option{-ftime-report}
+information is instead emitted in JSON form as part of SARIF output.  The
+precise format of this JSON data is subject to change, and the values may
+not exactly match those emitted to stderr due to being written out at a
+slightly different place within the compiler.
 
 @opindex ftime-report-details
 @item -ftime-report-details
diff --git a/gcc/testsuite/c-c++-common/diagnostic-format-sarif-file-timevars-1.c b/gcc/testsuite/c-c++-common/diagnostic-format-sarif-file-timevars-1.c
new file mode 100644
index 00000000000..be6b1e76bd4
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/diagnostic-format-sarif-file-timevars-1.c
@@ -0,0 +1,18 @@
+/* { dg-do compile } */
+/* { dg-options "-fdiagnostics-format=sarif-file -ftime-report" } */
+
+#warning message
+
+/* Verify that some JSON was written to a file with the expected name.  */
+/* { dg-final { verify-sarif-file } } */
+
+/* We expect various properties.
+   The indentation here reflects the expected hierarchy, though these tests
+   don't check for that, merely the string fragments we expect.
+
+   { dg-final { scan-sarif-file {"invocations": } } }
+     { dg-final { scan-sarif-file {"properties": } } }
+       { dg-final { scan-sarif-file {"gcc/timeReport": } } }
+         { dg-final { scan-sarif-file {"timevars": } } }
+           { dg-final { scan-sarif-file {"name": "TOTAL",} } }
+*/
diff --git a/gcc/testsuite/c-c++-common/diagnostic-format-sarif-file-timevars-2.c b/gcc/testsuite/c-c++-common/diagnostic-format-sarif-file-timevars-2.c
new file mode 100644
index 00000000000..e9c2b5e072e
--- /dev/null
+++ b/gcc/testsuite/c-c++-common/diagnostic-format-sarif-file-timevars-2.c
@@ -0,0 +1,21 @@
+/* As per diagnostic-format-sarif-file-timevars-1.c, but
+   adding -ftime-report-details.  */
+
+/* { dg-do compile } */
+/* { dg-options "-fdiagnostics-format=sarif-file -ftime-report -ftime-report-details" } */
+
+#warning message
+
+/* Verify that some JSON was written to a file with the expected name.  */
+/* { dg-final { verify-sarif-file } } */
+
+/* We expect various properties.
+   The indentation here reflects the expected hierarchy, though these tests
+   don't check for that, merely the string fragments we expect.
+
+   { dg-final { scan-sarif-file {"invocations": } } }
+     { dg-final { scan-sarif-file {"properties": } } }
+       { dg-final { scan-sarif-file {"gcc/timeReport": } } }
+         { dg-final { scan-sarif-file {"timevars": } } }
+           { dg-final { scan-sarif-file {"name": "TOTAL",} } }
+*/
diff --git a/gcc/timevar.cc b/gcc/timevar.cc
index d695297aae7..5368ff06ac9 100644
--- a/gcc/timevar.cc
+++ b/gcc/timevar.cc
@@ -23,6 +23,7 @@ along with GCC; see the file COPYING3.  If not see
 #include "coretypes.h"
 #include "timevar.h"
 #include "options.h"
+#include "json.h"
 
 #ifndef HAVE_CLOCK_T
 typedef int clock_t;
@@ -135,6 +136,8 @@ class timer::named_items
   void pop ();
   void print (FILE *fp, const timevar_time_def *total);
 
+  json::value *make_json () const;
+
  private:
   /* Which timer instance does this relate to?  */
   timer *m_timer;
@@ -143,7 +146,8 @@ class timer::named_items
      Note that currently we merely store/compare the raw string
      pointers provided by client code; we don't take a copy,
      or use strcmp.  */
-  hash_map <const char *, timer::timevar_def> m_hash_map;
+  typedef hash_map <const char *, timer::timevar_def> hash_map_t;
+  hash_map_t m_hash_map;
 
   /* The order in which items were originally inserted.  */
   auto_vec <const char *> m_names;
@@ -207,6 +211,23 @@ timer::named_items::print (FILE *fp, const timevar_time_def *total)
     }
 }
 
+/* Create a json value representing this object, suitable for use
+   in SARIF output.  */
+
+json::value *
+timer::named_items::make_json () const
+{
+  json::array *arr = new json::array ();
+  for (const char *item_name : m_names)
+    {
+      hash_map_t &mut_map = const_cast <hash_map_t &> (m_hash_map);
+      timer::timevar_def *def = mut_map.get (item_name);
+      gcc_assert (def);
+      arr->append (def->make_json ());
+    }
+  return arr;
+}
+
 /* Fill the current times into TIME.  The definition of this function
    also defines any or all of the HAVE_USER_TIME, HAVE_SYS_TIME, and
    HAVE_WALL_TIME macros.  */
@@ -251,6 +272,19 @@ timevar_accumulate (struct timevar_time_def *timer,
   timer->ggc_mem += stop_time->ggc_mem - start_time->ggc_mem;
 }
 
+/* Get the difference between STOP_TIME and START_TIME.  */
+
+static void
+timevar_diff (struct timevar_time_def *out,
+	      const timevar_time_def &start_time,
+	      const timevar_time_def &stop_time)
+{
+  out->user = stop_time.user - start_time.user;
+  out->sys = stop_time.sys - start_time.sys;
+  out->wall = stop_time.wall - start_time.wall;
+  out->ggc_mem = stop_time.ggc_mem - start_time.ggc_mem;
+}
+
 /* Class timer's constructor.  */
 
 timer::timer () :
@@ -791,6 +825,134 @@ timer::print (FILE *fp)
   validate_phases (fp);
 }
 
+/* Create a json value representing this object, suitable for use
+   in SARIF output.  */
+
+json::object *
+make_json_for_timevar_time_def (const timevar_time_def &ttd)
+{
+  json::object *obj = new json::object ();
+  obj->set ("user", new json::float_number (ttd.user));
+  obj->set ("sys", new json::float_number (ttd.sys));
+  obj->set ("wall", new json::float_number (ttd.wall));
+  obj->set ("ggc_mem", new json::integer_number (ttd.ggc_mem));
+  return obj;
+}
+
+/* Create a json value representing this object, suitable for use
+   in SARIF output.  */
+
+json::value *
+timer::timevar_def::make_json () const
+{
+  json::object *timevar_obj = new json::object ();
+  timevar_obj->set ("name", new json::string (name));
+  timevar_obj->set ("elapsed", make_json_for_timevar_time_def (elapsed));
+
+  if (children)
+    {
+      bool any_children_with_time = false;
+      for (auto i : *children)
+	if (!all_zero (i.second))
+	  {
+	    any_children_with_time = true;
+	    break;
+	  }
+      if (any_children_with_time)
+	{
+	  json::array *children_arr = new json::array ();
+	  timevar_obj->set ("children", children_arr);
+	  for (auto i : *children)
+	    {
+	      /* Don't emit timing variables if we're going to get a row of
+		 zeroes.  */
+	      if (all_zero (i.second))
+		continue;
+	      json::object *child_obj = new json::object;
+	      children_arr->append (child_obj);
+	      child_obj->set ("name", new json::string (i.first->name));
+	      child_obj->set ("elapsed",
+			      make_json_for_timevar_time_def (i.second));
+	    }
+	}
+    }
+
+  return timevar_obj;
+}
+
+/* Create a json value representing this object, suitable for use
+   in SARIF output.  */
+
+json::value *
+timer::make_json () const
+{
+  /* Only generate stuff if we have some sort of time information.  */
+#if defined (HAVE_USER_TIME) || defined (HAVE_SYS_TIME) || defined (HAVE_WALL_TIME)
+  json::object *report_obj = new json::object ();
+  json::array *json_arr = new json::array ();
+  report_obj->set ("timevars", json_arr);
+
+  for (unsigned id = 0; id < (unsigned int) TIMEVAR_LAST; ++id)
+    {
+      const timevar_def *tv = &m_timevars[(timevar_id_t) id];
+
+      /* Don't print the total execution time here; this isn't initialized
+	 by the time the sarif output runs.  */
+      if ((timevar_id_t) id == TV_TOTAL)
+	continue;
+
+      /* Don't emit timing variables that were never used.  */
+      if (!tv->used)
+	continue;
+
+      bool any_children_with_time = false;
+      if (tv->children)
+	for (child_map_t::iterator i = tv->children->begin ();
+	     i != tv->children->end (); ++i)
+	  if (! all_zero ((*i).second))
+	    {
+	      any_children_with_time = true;
+	      break;
+	    }
+
+      /* Don't emit timing variables if we're going to get a row of
+	 zeroes.  Unless there are children with non-zero time.  */
+      if (! any_children_with_time
+	  && all_zero (tv->elapsed))
+	continue;
+
+      json_arr->append (tv->make_json ());
+    }
+
+  /* Special-case for total.  */
+  {
+    /* Get our own total up till now, without affecting TV_TOTAL.  */
+    struct timevar_time_def total_now;
+    struct timevar_time_def total_elapsed;
+    get_time (&total_now);
+    timevar_diff (&total_elapsed, m_timevars[TV_TOTAL].start_time, total_now);
+
+    json::object *total_obj = new json::object();
+    json_arr->append (total_obj);
+    total_obj->set ("name", new json::string ("TOTAL"));
+    total_obj->set ("elapsed", make_json_for_timevar_time_def (total_elapsed));
+  }
+
+  if (m_jit_client_items)
+    report_obj->set ("client_items", m_jit_client_items->make_json ());
+
+  report_obj->set ("CHECKING_P", new json::literal ((bool)CHECKING_P));
+  report_obj->set ("flag_checking", new json::literal ((bool)flag_checking));
+
+  return report_obj;
+
+#else /* defined (HAVE_USER_TIME) || defined (HAVE_SYS_TIME)
+	  || defined (HAVE_WALL_TIME) */
+  return NULL;
+#endif /* defined (HAVE_USER_TIME) || defined (HAVE_SYS_TIME)
+	  || defined (HAVE_WALL_TIME) */
+}
+
 /* Get the name of the topmost item.  For use by jit for validating
    inputs to gcc_jit_timer_pop.  */
 const char *
diff --git a/gcc/timevar.h b/gcc/timevar.h
index ad465731609..e359e9fa1fa 100644
--- a/gcc/timevar.h
+++ b/gcc/timevar.h
@@ -21,6 +21,8 @@
 #ifndef GCC_TIMEVAR_H
 #define GCC_TIMEVAR_H
 
+namespace json { class value; }
+
 /* Timing variables are used to measure elapsed time in various
    portions of the compiler.  Each measures elapsed user, system, and
    wall-clock time, as appropriate to and supported by the host
@@ -119,6 +121,7 @@ class timer
   void pop_client_item ();
 
   void print (FILE *fp);
+  json::value *make_json () const;
 
   const char *get_topmost_item_name () const;
 
@@ -140,6 +143,8 @@ class timer
   /* Private type: a timing variable.  */
   struct timevar_def
   {
+    json::value *make_json () const;
+
     /* Elapsed time for this variable.  */
     struct timevar_time_def elapsed;
 
diff --git a/gcc/tree-diagnostic-client-data-hooks.cc b/gcc/tree-diagnostic-client-data-hooks.cc
index 1a35f4cb122..fc972ce0e13 100644
--- a/gcc/tree-diagnostic-client-data-hooks.cc
+++ b/gcc/tree-diagnostic-client-data-hooks.cc
@@ -1,5 +1,5 @@
 /* Implementation of diagnostic_client_data_hooks for the compilers
-   (e.g. with knowledge of "tree" and lang_hooks).
+   (e.g. with knowledge of "tree", lang_hooks, and timevars).
    Copyright (C) 2022-2023 Free Software Foundation, Inc.
    Contributed by David Malcolm <dmalcolm@redhat.com>.
 
@@ -27,8 +27,10 @@ along with GCC; see the file COPYING3.  If not see
 #include "diagnostic.h"
 #include "tree-logical-location.h"
 #include "diagnostic-client-data-hooks.h"
+#include "diagnostic-format-sarif.h"
 #include "langhooks.h"
 #include "plugin.h"
+#include "timevar.h"
 
 /* Concrete class for supplying a diagnostic_context with information
    about a specific plugin within the client, when the client is the
@@ -111,7 +113,7 @@ private:
 };
 
 /* Subclass of diagnostic_client_data_hooks for use by compilers proper
-   i.e. with knowledge of "tree", access to langhooks, etc.  */
+   i.e. with knowledge of "tree", access to langhooks, timevars etc.  */
 
 class compiler_data_hooks : public diagnostic_client_data_hooks
 {
@@ -135,6 +137,26 @@ public:
     return lang_hooks.get_sarif_source_language (filename);
   }
 
+  void
+  add_sarif_invocation_properties (sarif_object &invocation_obj)
+    const final override
+  {
+    if (g_timer)
+      if (json::value *timereport_val = g_timer->make_json ())
+	{
+	  sarif_property_bag &bag_obj
+	    = invocation_obj.get_or_create_properties ();
+	  bag_obj.set ("gcc/timeReport", timereport_val);
+
+	  /* If the user requested SARIF output, then assume they want the
+	     time report data in the SARIF output, and *not* later emitted on
+	     stderr.
+	     Implement this by cleaning up the global timer instance now.  */
+	  delete g_timer;
+	  g_timer = NULL;
+	}
+  }
+
 private:
   compiler_version_info m_version_info;
   current_fndecl_logical_location m_current_fndecl_logical_loc;
