diff --git a/hadrian/src/Context.hs b/hadrian/src/Context.hs
index 7c7bb124ff8fd0969ca5557600eab2cc386dac2a..0676743ee588085e227b5a15d209141672940b90 100644
--- a/hadrian/src/Context.hs
+++ b/hadrian/src/Context.hs
@@ -8,7 +8,8 @@ module Context (
     -- * Paths
     contextDir, buildPath, buildDir, pkgInplaceConfig, pkgSetupConfigFile,
     pkgHaddockFile, pkgRegisteredLibraryFile, pkgLibraryFile, pkgGhciLibraryFile,
-    pkgConfFile, objectPath, contextPath, getContextPath, libPath, distDir
+    pkgConfFile, objectPath, contextPath, getContextPath, libPath, distDir,
+    haddockStatsFilesDir
     ) where
 
 import Base
diff --git a/hadrian/src/Context/Path.hs b/hadrian/src/Context/Path.hs
index 4bc9d9be34f5d824657731236560e128459ae968..e5a82710ab97887aefeb1b6440beddc1c8ba2bd7 100644
--- a/hadrian/src/Context/Path.hs
+++ b/hadrian/src/Context/Path.hs
@@ -41,3 +41,8 @@ buildPath context = buildRoot <&> (-/- buildDir context)
 -- | The expression that evaluates to the build path of the current 'Context'.
 getBuildPath :: Expr Context b FilePath
 getBuildPath = expr . buildPath =<< getContext
+
+-- | Path to the directory containing haddock timing files, used by
+--   the haddock perf tests.
+haddockStatsFilesDir :: Action FilePath
+haddockStatsFilesDir = (-/- "stage1" -/- "haddock-timing-files") <$> buildRoot
diff --git a/hadrian/src/Rules/Documentation.hs b/hadrian/src/Rules/Documentation.hs
index a70b4d5c9d400c480116d2ec5b48b43585ba4d58..9991f017080d06784be52f8c3ca59a08c0d4b1dc 100644
--- a/hadrian/src/Rules/Documentation.hs
+++ b/hadrian/src/Rules/Documentation.hs
@@ -205,7 +205,12 @@ buildPackageDocumentation = do
         -- TODO: Pass the correct way from Rules via Context.
         dynamicPrograms <- dynamicGhcPrograms =<< flavour
         let haddockWay = if dynamicPrograms then dynamic else vanilla
+        statsFilesDir <- haddockStatsFilesDir
+        createDirectory statsFilesDir
         build $ target (context {way = haddockWay}) (Haddock BuildPackage) srcs [file]
+        produces [
+          statsFilesDir </> pkgName (Context.package context) <.> "t"
+          ]
 
 data PkgDocTarget = DotHaddock PackageName | HaddockPrologue PackageName
   deriving (Eq, Show)
diff --git a/hadrian/src/Settings/Builders/Haddock.hs b/hadrian/src/Settings/Builders/Haddock.hs
index 65c703160cda32674ecd519b9a40dcca1b154a46..b01b8a2459027a37ea2a10a7c7483ae62bce90c7 100644
--- a/hadrian/src/Settings/Builders/Haddock.hs
+++ b/hadrian/src/Settings/Builders/Haddock.hs
@@ -35,13 +35,13 @@ haddockBuilderArgs = mconcat
         output   <- getOutput
         pkg      <- getPackage
         root     <- getBuildRoot
-        path     <- getBuildPath
         context  <- getContext
         version  <- expr $ pkgVersion  pkg
         synopsis <- expr $ pkgSynopsis pkg
         deps     <- getContextData depNames
         haddocks <- expr $ haddockDependencies context
         hVersion <- expr $ pkgVersion haddock
+        statsDir <- expr $ haddockStatsFilesDir
         ghcOpts  <- haddockGhcArgs
         mconcat
             [ arg "--verbosity=0"
@@ -66,6 +66,6 @@ haddockBuilderArgs = mconcat
             , pure [ "--optghc=" ++ opt | opt <- ghcOpts, not ("--package-db" `isInfixOf` opt) ]
             , getInputs
             , arg "+RTS"
-            , arg $ "-t" ++ path -/- "haddock.t"
+            , arg $ "-t" ++ (statsDir -/- pkgName pkg ++ ".t")
             , arg "--machine-readable"
             , arg "-RTS" ] ]
diff --git a/hadrian/src/Settings/Builders/RunTest.hs b/hadrian/src/Settings/Builders/RunTest.hs
index 6cc11f8aef5a23035df60b92ba05e85b4c93296e..63e3dfd6c951db80ba94e718ed6e75941b8b1b71 100644
--- a/hadrian/src/Settings/Builders/RunTest.hs
+++ b/hadrian/src/Settings/Builders/RunTest.hs
@@ -85,11 +85,14 @@ runTestBuilderArgs = builder RunTest ? do
     wordsize    <- getTestSetting TestWORDSIZE
     top         <- expr $ topDirectory
     ghcFlags    <- expr runTestGhcFlags
-    timeoutProg <- expr buildRoot <&> (-/- timeoutPath)
     cmdrootdirs <- expr (testRootDirs <$> userSetting defaultTestArgs)
     let defaultRootdirs = ("testsuite" -/- "tests") : libTests
         rootdirs | null cmdrootdirs = defaultRootdirs
                  | otherwise        = cmdrootdirs
+    root        <- expr buildRoot
+    let timeoutProg = root -/- timeoutPath
+    statsFilesDir <- expr haddockStatsFilesDir
+
     -- See #16087
     let ghcBuiltByLlvm = False -- TODO: Implement this check
 
@@ -134,6 +137,7 @@ runTestBuilderArgs = builder RunTest ? do
 
             , arg "--config", arg $ "gs=gs"                           -- Use the default value as in test.mk
             , arg "--config", arg $ "timeout_prog=" ++ show (top -/- timeoutProg)
+            , arg "--config", arg $ "stats_files_dir=" ++ statsFilesDir
             , arg $ "--threads=" ++ show threads
             , getTestArgs -- User-provided arguments from command line.
             ]
diff --git a/rules/haddock.mk b/rules/haddock.mk
index f0d686194fec9c3d9eee74e974fc24865cb4572d..a0e4f1724ab78291e649831c189141abe4b90b95 100644
--- a/rules/haddock.mk
+++ b/rules/haddock.mk
@@ -76,7 +76,7 @@ endif
 		$$($1_$2_HS_SRCS) \
 		$$($1_$2_EXTRA_HADDOCK_SRCS) \
 		$$(EXTRA_HADDOCK_OPTS) \
-		+RTS -t"$1/$2/haddock.t" --machine-readable
+		+RTS -t"$$(TOP)/testsuite/tests/perf/haddock/$$($1_PACKAGE).t" --machine-readable
 
 # --no-tmp-comp-dir above is important: it saves a few minutes in a
 # validate.  This flag lets Haddock use the pre-compiled object files
diff --git a/testsuite/driver/testglobals.py b/testsuite/driver/testglobals.py
index b28477c76904be13fec560e5fc769acb598f42f1..ababefba195410131ba178c91797c40707aa2702 100644
--- a/testsuite/driver/testglobals.py
+++ b/testsuite/driver/testglobals.py
@@ -139,6 +139,11 @@ class TestConfig:
         # terminal supports colors
         self.supports_colors = False
 
+        # Where to look up runtime stats produced by haddock, needed for
+        # the haddock perf tests in testsuite/tests/perf/haddock/.
+        # See Note [Haddock runtime stats files] at the bottom of this file.
+        self.stats_files_dir = '/please_set_stats_files_dir'
+
 global config
 config = TestConfig()
 
diff --git a/testsuite/driver/testlib.py b/testsuite/driver/testlib.py
index dc8b1b85f143a48568756f3e5506667017e7b7da..d0bd98015dd47c2f2da0b8af041d48074d4e9978 100644
--- a/testsuite/driver/testlib.py
+++ b/testsuite/driver/testlib.py
@@ -67,7 +67,6 @@ def isStatsTest():
     opts = getTestOpts()
     return opts.is_stats_test
 
-
 # This can be called at the top of a file of tests, to set default test options
 # for the following tests.
 def setTestOpts( f ):
@@ -1211,7 +1210,11 @@ def multi_compile_and_run( name, way, top_mod, extra_mods, extra_hc_opts ):
 
 def stats( name, way, stats_file ):
     opts = getTestOpts()
-    return check_stats(name, way, stats_file, opts.stats_range_fields)
+    return check_stats(name, way, in_testdir(stats_file), opts.stats_range_fields)
+
+def static_stats( name, way, stats_file ):
+    opts = getTestOpts()
+    return check_stats(name, way, in_statsdir(stats_file), opts.stats_range_fields)
 
 def metric_dict(name, way, metric, value):
     return Perf.PerfStat(
@@ -1234,7 +1237,7 @@ def check_stats(name, way, stats_file, range_fields):
     result = passed()
     if range_fields:
         try:
-            f = open(in_testdir(stats_file))
+            f = open(stats_file)
         except IOError as e:
             return failBecause(str(e))
         stats_file_contents = f.read()
@@ -1357,7 +1360,7 @@ def simple_build(name, way, extra_hc_opts, should_fail, top_mod, link, addsuf, b
     # ToDo: if the sub-shell was killed by ^C, then exit
 
     if isCompilerStatsTest():
-        statsResult = check_stats(name, way, stats_file, opts.stats_range_fields)
+        statsResult = check_stats(name, way, in_testdir(stats_file), opts.stats_range_fields)
         if badResult(statsResult):
             return statsResult
 
@@ -1442,7 +1445,7 @@ def simple_run(name, way, prog, extra_run_opts):
     if check_prof and not check_prof_ok(name, way):
         return failBecause('bad profile')
 
-    return check_stats(name, way, stats_file, opts.stats_range_fields)
+    return check_stats(name, way, in_testdir(stats_file), opts.stats_range_fields)
 
 def rts_flags(way):
     args = config.way_rts_flags.get(way, [])
@@ -2103,6 +2106,9 @@ def in_testdir(name, suffix=''):
 def in_srcdir(name, suffix=''):
     return os.path.join(getTestOpts().srcdir, add_suffix(name, suffix))
 
+def in_statsdir(name, suffix=''):
+    return os.path.join(config.stats_files_dir, add_suffix(name, suffix))
+
 # Finding the sample output.  The filename is of the form
 #
 #   <test>.stdout[-ws-<wordsize>][-<platform>|-<os>]
diff --git a/testsuite/mk/test.mk b/testsuite/mk/test.mk
index b1d716fd77888495b0070a4cc7e1a394a200d8b3..6860974c54e377c0b19af95b96c96e8544baac9d 100644
--- a/testsuite/mk/test.mk
+++ b/testsuite/mk/test.mk
@@ -278,6 +278,8 @@ RUNTEST_OPTS +=  \
 	--config 'gs=$(call quote_path,$(GS))' \
 	--config 'timeout_prog=$(call quote_path,$(TIMEOUT_PROGRAM))'
 
+RUNTEST_OPTS += --config 'stats_files_dir=$(TOP)/tests/perf/haddock'
+
 RUNTEST_OPTS += -e "config.stage=$(GhcStage)"
 
 ifneq "$(METRICS_FILE)" ""
diff --git a/testsuite/tests/perf/haddock/all.T b/testsuite/tests/perf/haddock/all.T
index fca30366f954f4e2884a074365642383ef97f444..929a7fd972f2334dac9c6f173948f82a36819655 100644
--- a/testsuite/tests/perf/haddock/all.T
+++ b/testsuite/tests/perf/haddock/all.T
@@ -1,27 +1,39 @@
+# Note [Haddock runtime stats files]
+#
+# When one of the build systems builds a complete GHC distribution,
+# haddock gets built and then used to generate .haddock files for each
+# library. For that last step, both build systems pass an extra
+# `+RTS -t<some path>.t` to record runtime statistics to the given path.
+#
+# Those .t files are then used by a few haddock perf tests (which all live
+# under testsuite/tests/perf/haddock/). Since each build system needs to produce
+# those files in different places, the testsuite driver takes the directory
+# under which those files are placed as a configuration parameter,
+# `config.stats_files_dir`. Each individual test then specifies the name of
+# the (runtime statistics) file against which some checks are to be performed,
+# in addition to declaring the test's type to be `static_stats`.
+
 # We do not add peak_megabytes_allocated and max_bytes_used to these tests, as
 # they are somewhat unreliable, and it is harder to re-run these numbers to
 # detect outliers, as described in Note [residency]. See #9556.
 
 test('haddock.base',
-     [extra_files(['../../../../libraries/base/dist-install/haddock.t']),
-      unless(in_tree_compiler(), skip), req_haddock
+     [unless(in_tree_compiler(), skip), req_haddock
      ,collect_stats('bytes allocated',5)
       ],
-     stats,
-     ['haddock.t'])
+     static_stats,
+     ['base.t'])
 
 test('haddock.Cabal',
-     [extra_files(['../../../../libraries/Cabal/Cabal/dist-install/haddock.t']),
-      unless(in_tree_compiler(), skip), req_haddock
+     [unless(in_tree_compiler(), skip), req_haddock
      ,collect_stats('bytes allocated',5)
       ],
-     stats,
-     ['haddock.t'])
+     static_stats,
+     ['Cabal.t'])
 
 test('haddock.compiler',
-     [extra_files(['../../../../compiler/stage2/haddock.t']),
-      unless(in_tree_compiler(), skip), req_haddock
+     [unless(in_tree_compiler(), skip), req_haddock
      ,collect_stats('bytes allocated',10)
       ],
-     stats,
-     ['haddock.t'])
+     static_stats,
+     ['ghc.t'])