git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v7 6/7] ieot: add Index Entry Offset Table (IEOT) extension

From
Ben Peart <peartben@gmail.com>
Date
Oct 1, 2018, 13:45 UTC
Message-ID
<20181001134556.33232-7-peartben@gmail.com>
In-Reply-To
<20181001134556.33232-1-peartben@gmail.com>
From: Ben Peart <benpeart@microsoft.com>

This patch enables addressing the CPU cost of loading the index by adding additional data to the index that will allow us to efficiently multi- thread the loading and conversion of cache entries.

It accomplishes this by adding an (optional) index extension that is a table of offsets to blocks of cache entries in the index file. To make this work for V4 indexes, when writing the cache entries, it periodically "resets" the prefix-compression by encoding the current entry as if the path name for the previous entry is completely different and saves the offset of that entry in the IEOT. Basically, with V4 indexes, it generates offsets into blocks of prefix-compressed entries.

Signed-off-by: Ben Peart <peartben@gmail.com>
---
 Documentation/technical/index-format.txt |  18 +++
 read-cache.c                             | 173 +++++++++++++++++++++++
 2 files changed, 191 insertions(+)
diff --git a/Documentation/technical/index-format.txt b/Documentation/technical/index-format.txt
index 6bc2d90f7f..7c4d67aa6a 100644
--- a/Documentation/technical/index-format.txt
+++ b/Documentation/technical/index-format.txt
@@ -337,3 +337,21 @@ The remaining data of each directory block is grouped by type:
 
 	SHA-1("TREE" + <binary representation of N> +
 		"REUC" + <binary representation of M>)
+
+== Index Entry Offset Table
+
+  The Index Entry Offset Table (IEOT) is used to help address the CPU
+  cost of loading the index by enabling multi-threading the process of
+  converting cache entries from the on-disk format to the in-memory format.
+  The signature for this extension is { 'I', 'E', 'O', 'T' }.
+
+  The extension consists of:
+
+  - 32-bit version (currently 1)
+
+  - A number of index offset entries each consisting of:
+
+    - 32-bit offset from the begining of the file to the first cache entry
+	in this block of entries.
+
+    - 32-bit count of cache entries in this block
diff --git a/read-cache.c b/read-cache.c
index 77083ab8bb..9557376e78 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -45,6 +45,7 @@
 #define CACHE_EXT_UNTRACKED 0x554E5452	  /* "UNTR" */
 #define CACHE_EXT_FSMONITOR 0x46534D4E	  /* "FSMN" */
 #define CACHE_EXT_ENDOFINDEXENTRIES 0x454F4945	/* "EOIE" */
+#define CACHE_EXT_INDEXENTRYOFFSETTABLE 0x49454F54 /* "IEOT" */
 
 /* changes that can be kept in $GIT_DIR/index (basically all extensions) */
 #define EXTMASK (RESOLVE_UNDO_CHANGED | CACHE_TREE_CHANGED | \
@@ -1696,6 +1697,7 @@ static int read_index_extension(struct index_state *istate,
 		read_fsmonitor_extension(istate, data, sz);
 		break;
 	case CACHE_EXT_ENDOFINDEXENTRIES:
+	case CACHE_EXT_INDEXENTRYOFFSETTABLE:
 		/* already handled in do_read_index() */
 		break;
 	default:
@@ -1888,6 +1890,23 @@ static size_t estimate_cache_size(size_t ondisk_size, unsigned int entries)
 	return ondisk_size + entries * per_entry;
 }
 
+struct index_entry_offset
+{
+	/* starting byte offset into index file, count of index entries in this block */
+	int offset, nr;
+};
+
+struct index_entry_offset_table
+{
+	int nr;
+	struct index_entry_offset entries[0];
+};
+
+#ifndef NO_PTHREADS
+static struct index_entry_offset_table *read_ieot_extension(const char *mmap, size_t mmap_size, size_t offset);
+static void write_ieot_extension(struct strbuf *sb, struct index_entry_offset_table *ieot);
+#endif
+
 static size_t read_eoie_extension(const char *mmap, size_t mmap_size);
 static void write_eoie_extension(struct strbuf *sb, git_hash_ctx *eoie_context, size_t offset);
 
@@ -1931,6 +1950,15 @@ static void *load_index_extensions(void *_data)
 	return NULL;
 }
 
+/*
+ * Mostly randomly chosen maximum thread counts: we
+ * cap the parallelism to online_cpus() threads, and we want
+ * to have at least 10000 cache entries per thread for it to
+ * be worth starting a thread.
+ */
+
+#define THREAD_COST		(10000)
+
 /* remember to discard_cache() before reading a different cache! */
 int do_read_index(struct index_state *istate, const char *path, int must_exist)
 {
@@ -2523,6 +2551,9 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile,
 	struct strbuf previous_name_buf = STRBUF_INIT, *previous_name;
 	int drop_cache_tree = istate->drop_cache_tree;
 	off_t offset;
+	int ieot_work = 1;
+	struct index_entry_offset_table *ieot = NULL;
+	int nr;
 
 	for (i = removed = extended = 0; i < entries; i++) {
 		if (cache[i]->ce_flags & CE_REMOVE)
@@ -2556,7 +2587,38 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile,
 	if (ce_write(&c, newfd, &hdr, sizeof(hdr)) < 0)
 		return -1;
 
+#ifndef NO_PTHREADS
+	if ((nr = git_config_get_index_threads()) != 1) {
+		int ieot_blocks, cpus;
+
+		/*
+		 * ensure default number of ieot blocks maps evenly to the
+		 * default number of threads that will process them
+		 */
+		if (!nr) {
+			ieot_blocks = istate->cache_nr / THREAD_COST;
+			cpus = online_cpus();
+			if (ieot_blocks > cpus - 1)
+				ieot_blocks = cpus - 1;
+		} else {
+			ieot_blocks = nr;
+		}
+
+		/*
+		 * no reason to write out the IEOT extension if we don't
+		 * have enough blocks to utilize multi-threading
+		 */
+		if (ieot_blocks > 1) {
+			ieot = xcalloc(1, sizeof(struct index_entry_offset_table)
+				+ (ieot_blocks * sizeof(struct index_entry_offset)));
+			ieot->nr = 0;
+			ieot_work = DIV_ROUND_UP(entries, ieot_blocks);
+		}
+	}
+#endif
+
 	offset = lseek(newfd, 0, SEEK_CUR) + write_buffer_len;
+	nr = 0;
 	previous_name = (hdr_version == 4) ? &previous_name_buf : NULL;
 
 	for (i = 0; i < entries; i++) {
@@ -2578,11 +2640,31 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile,
 
 			drop_cache_tree = 1;
 		}
+		if (ieot && i && (i % ieot_work == 0)) {
+			ieot->entries[ieot->nr].nr = nr;
+			ieot->entries[ieot->nr].offset = offset;
+			ieot->nr++;
+			/*
+			 * If we have a V4 index, set the first byte to an invalid
+			 * character to ensure there is nothing common with the previous
+			 * entry
+			 */
+			if (previous_name)
+				previous_name->buf[0] = 0;
+			nr = 0;
+			offset = lseek(newfd, 0, SEEK_CUR) + write_buffer_len;
+		}
 		if (ce_write_entry(&c, newfd, ce, previous_name, (struct ondisk_cache_entry *)&ondisk) < 0)
 			err = -1;
 
 		if (err)
 			break;
+		nr++;
+	}
+	if (ieot && nr) {
+		ieot->entries[ieot->nr].nr = nr;
+		ieot->entries[ieot->nr].offset = offset;
+		ieot->nr++;
 	}
 	strbuf_release(&previous_name_buf);
 
@@ -2593,6 +2675,26 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile,
 	offset = lseek(newfd, 0, SEEK_CUR) + write_buffer_len;
 	the_hash_algo->init_fn(&eoie_c);
 
+	/*
+	 * Lets write out CACHE_EXT_INDEXENTRYOFFSETTABLE first so that we
+	 * can minimze the number of extensions we have to scan through to
+	 * find it during load.  Write it out regardless of the
+	 * strip_extensions parameter as we need it when loading the shared
+	 * index.
+	 */
+#ifndef NO_PTHREADS
+	if (ieot) {
+		struct strbuf sb = STRBUF_INIT;
+
+		write_ieot_extension(&sb, ieot);
+		err = write_index_ext_header(&c, &eoie_c, newfd, CACHE_EXT_INDEXENTRYOFFSETTABLE, sb.len) < 0
+			|| ce_write(&c, newfd, sb.buf, sb.len) < 0;
+		strbuf_release(&sb);
+		if (err)
+			return -1;
+	}
+#endif
+
 	if (!strip_extensions && istate->split_index) {
 		struct strbuf sb = STRBUF_INIT;
 
@@ -3176,3 +3278,74 @@ static void write_eoie_extension(struct strbuf *sb, git_hash_ctx *eoie_context,
 	the_hash_algo->final_fn(hash, eoie_context);
 	strbuf_add(sb, hash, the_hash_algo->rawsz);
 }
+
+#ifndef NO_PTHREADS
+#define IEOT_VERSION	(1)
+
+static struct index_entry_offset_table *read_ieot_extension(const char *mmap, size_t mmap_size, size_t offset)
+{
+       const char *index = NULL;
+       uint32_t extsize, ext_version;
+       struct index_entry_offset_table *ieot;
+       int i, nr;
+
+       /* find the IEOT extension */
+       if (!offset)
+	       return NULL;
+       while (offset <= mmap_size - the_hash_algo->rawsz - 8) {
+	       extsize = get_be32(mmap + offset + 4);
+	       if (CACHE_EXT((mmap + offset)) == CACHE_EXT_INDEXENTRYOFFSETTABLE) {
+		       index = mmap + offset + 4 + 4;
+		       break;
+	       }
+	       offset += 8;
+	       offset += extsize;
+       }
+       if (!index)
+	       return NULL;
+
+       /* validate the version is IEOT_VERSION */
+       ext_version = get_be32(index);
+       if (ext_version != IEOT_VERSION)
+	       return NULL;
+       index += sizeof(uint32_t);
+
+       /* extension size - version bytes / bytes per entry */
+       nr = (extsize - sizeof(uint32_t)) / (sizeof(uint32_t) + sizeof(uint32_t));
+       if (!nr)
+	       return NULL;
+       ieot = xmalloc(sizeof(struct index_entry_offset_table)
+	       + (nr * sizeof(struct index_entry_offset)));
+       ieot->nr = nr;
+       for (i = 0; i < nr; i++) {
+	       ieot->entries[i].offset = get_be32(index);
+	       index += sizeof(uint32_t);
+	       ieot->entries[i].nr = get_be32(index);
+	       index += sizeof(uint32_t);
+       }
+
+       return ieot;
+}
+
+static void write_ieot_extension(struct strbuf *sb, struct index_entry_offset_table *ieot)
+{
+       uint32_t buffer;
+       int i;
+
+       /* version */
+       put_be32(&buffer, IEOT_VERSION);
+       strbuf_add(sb, &buffer, sizeof(uint32_t));
+
+       /* ieot */
+       for (i = 0; i < ieot->nr; i++) {
+
+	       /* offset */
+	       put_be32(&buffer, ieot->entries[i].offset);
+	       strbuf_add(sb, &buffer, sizeof(uint32_t));
+
+	       /* count */
+	       put_be32(&buffer, ieot->entries[i].nr);
+	       strbuf_add(sb, &buffer, sizeof(uint32_t));
+       }
+}
+#endif
-- 
2.18.0.windows.1
Previous: Ben PeartNext: Duy Nguyen
Message 124 of 199 in “read-cache: speed up index load through parallelization”
  1. read-cache: speed up index load through parallelizationBen Peart, Aug 23, 2018
  2. Stefan BellerAug 23, 2018
  3. Ben PeartAug 23, 2018
  4. Duy NguyenAug 24, 2018
  5. Ben PeartAug 28, 2018
  6. Junio C HamanoAug 23, 2018
  7. Ben PeartAug 23, 2018
  8. Duy NguyenAug 24, 2018
  9. Duy NguyenAug 24, 2018
  10. Ben PeartAug 24, 2018
  11. read-cache.c: optimize reading index format v4Nguyễn Thái Ngọc Duy, Aug 25, 2018
  12. Junio C HamanoAug 27, 2018
  13. Duy NguyenAug 28, 2018
  14. Ben PeartAug 28, 2018
  15. Junio C HamanoAug 29, 2018
  16. Duy NguyenSep 4, 2018
  17. 0/1 optimize reading index format v4Nguyễn Thái Ngọc Duy, Sep 2, 2018
  18. 1/1 read-cache.c: optimize reading index format v4Nguyễn Thái Ngọc Duy, Sep 2, 2018
  19. Junio C HamanoSep 4, 2018
  20. Junio C HamanoSep 4, 2018
  21. Duy NguyenAug 24, 2018
  22. Ben PeartAug 24, 2018
  23. Duy NguyenAug 24, 2018
  24. Ben PeartAug 24, 2018
  25. 0/3 read-cache: speed up index load through parallelizationBen Peart, Aug 29, 2018
  26. 2/3 read-cache: load cache extensions on worker threadBen Peart, Aug 29, 2018
  27. Junio C HamanoAug 29, 2018
  28. Ben PeartAug 29, 2018
  29. Junio C HamanoAug 29, 2018
  30. Duy NguyenSep 3, 2018
  31. Duy NguyenSep 3, 2018
  32. 3/3 read-cache: micro-optimize expand_name_field() to speed up V4 index parsing.Ben Peart, Aug 29, 2018
  33. 1/3 read-cache: speed up index load through parallelizationBen Peart, Aug 29, 2018
  34. Junio C HamanoAug 29, 2018
  35. Ben PeartAug 29, 2018
  36. Duy NguyenSep 3, 2018
  37. 0/4 read-cache: speed up index load through parallelizationBen Peart, Sep 6, 2018
  38. 1/4 read-cache: optimize expand_name_field() to speed up V4 index parsing.Ben Peart, Sep 6, 2018
  39. 2/4 eoie: add End of Index Entry (EOIE) extensionBen Peart, Sep 6, 2018
  40. Junio C HamanoSep 7, 2018
  41. Ben PeartSep 7, 2018
  42. Martin ÅgrenSep 8, 2018
  43. Ben PeartSep 8, 2018
  44. Martin ÅgrenSep 8, 2018
  45. 3/4 read-cache: load cache extensions on a worker threadBen Peart, Sep 6, 2018
  46. Junio C HamanoSep 7, 2018
  47. Ben PeartSep 8, 2018
  48. 4/4 read-cache: speed up index load through parallelizationBen Peart, Sep 6, 2018
  49. Torsten BögershausenSep 7, 2018
  50. Ben PeartSep 7, 2018
  51. Junio C HamanoSep 7, 2018
  52. Ben PeartSep 7, 2018
  53. Duy NguyenSep 8, 2018
  54. 0/5 read-cache: speed up index load through parallelizationBen Peart, Sep 11, 2018
  55. 1/5 eoie: add End of Index Entry (EOIE) extensionBen Peart, Sep 11, 2018
  56. 2/5 read-cache: load cache extensions on a worker threadBen Peart, Sep 11, 2018
  57. 3/5 read-cache: speed up index load through parallelizationBen Peart, Sep 11, 2018
  58. 4/5 read-cache.c: optimize reading index format v4Ben Peart, Sep 11, 2018
  59. 5/5 read-cache: clean up casting and byte decodingBen Peart, Sep 11, 2018
  60. Ben PeartSep 12, 2018
  61. 0/5 read-cache: speed up index load through parallelizationBen Peart, Sep 12, 2018
  62. 2/5 read-cache: load cache extensions on a worker threadBen Peart, Sep 12, 2018
  63. Duy NguyenSep 15, 2018
  64. Duy NguyenSep 15, 2018
  65. Ben PeartSep 17, 2018
  66. Duy NguyenSep 15, 2018
  67. Junio C HamanoSep 17, 2018
  68. Ben PeartSep 17, 2018
  69. Duy NguyenSep 17, 2018
  70. Junio C HamanoSep 17, 2018
  71. 3/5 read-cache: load cache entries on worker threadsBen Peart, Sep 12, 2018
  72. Duy NguyenSep 15, 2018
  73. Ben PeartSep 17, 2018
  74. Duy NguyenSep 15, 2018
  75. Duy NguyenSep 15, 2018
  76. Ben PeartSep 17, 2018
  77. Duy NguyenSep 15, 2018
  78. 4/5 read-cache.c: optimize reading index format v4Ben Peart, Sep 12, 2018
  79. 5/5 read-cache: clean up casting and byte decodingBen Peart, Sep 12, 2018
  80. 1/5 eoie: add End of Index Entry (EOIE) extensionBen Peart, Sep 12, 2018
  81. Junio C HamanoSep 13, 2018
  82. Duy NguyenSep 15, 2018
  83. Ben PeartSep 17, 2018
  84. Duy NguyenSep 17, 2018
  85. Junio C HamanoSep 17, 2018
  86. Duy NguyenSep 17, 2018
  87. Junio C HamanoSep 17, 2018
  88. 0/7 speed up index load through parallelizationBen Peart, Sep 26, 2018
  89. 1/7 read-cache.c: optimize reading index format v4Ben Peart, Sep 26, 2018
  90. 2/7 read-cache: clean up casting and byte decodingBen Peart, Sep 26, 2018
  91. 3/7 eoie: add End of Index Entry (EOIE) extensionBen Peart, Sep 26, 2018
  92. SZEDER GáborSep 28, 2018
  93. Ben PeartSep 28, 2018
  94. SZEDER GáborSep 29, 2018
  95. Duy NguyenSep 29, 2018
  96. Junio C HamanoSep 29, 2018
  97. 4/7 config: add new index.threads config settingBen Peart, Sep 26, 2018
  98. SZEDER GáborSep 28, 2018
  99. Ben PeartSep 28, 2018
  100. Junio C HamanoSep 28, 2018
  101. Ben PeartSep 28, 2018
  102. Ramsay JonesSep 28, 2018
  103. Junio C HamanoSep 28, 2018
  104. Ben PeartOct 1, 2018
  105. SZEDER GáborOct 1, 2018
  106. 5/7 read-cache: load cache extensions on a worker threadBen Peart, Sep 26, 2018
  107. 6/7 ieot: add Index Entry Offset Table (IEOT) extensionBen Peart, Sep 26, 2018
  108. 7/7 read-cache: load cache entries on worker threadsBen Peart, Sep 26, 2018
  109. Junio C HamanoSep 26, 2018
  110. Duy NguyenSep 27, 2018
  111. 0/7 speed up index load through parallelizationBen Peart, Oct 1, 2018
  112. 1/7 read-cache.c: optimize reading index format v4Ben Peart, Oct 1, 2018
  113. 2/7 read-cache: clean up casting and byte decodingBen Peart, Oct 1, 2018
  114. Duy NguyenOct 1, 2018
  115. 3/7 eoie: add End of Index Entry (EOIE) extensionBen Peart, Oct 1, 2018
  116. SZEDER GáborOct 1, 2018
  117. Ben PeartOct 2, 2018
  118. Duy NguyenOct 1, 2018
  119. Ben PeartOct 2, 2018
  120. 4/7 config: add new index.threads config settingBen Peart, Oct 1, 2018
  121. 5/7 read-cache: load cache extensions on a worker threadBen Peart, Oct 1, 2018
  122. Duy NguyenOct 1, 2018
  123. Ben PeartOct 2, 2018
  124. 6/7 ieot: add Index Entry Offset Table (IEOT) extensionBen Peart, Oct 1, 2018
  125. Duy NguyenOct 1, 2018
  126. Ben PeartOct 2, 2018
  127. Duy NguyenOct 2, 2018
  128. 7/7 read-cache: load cache entries on worker threadsBen Peart, Oct 1, 2018
  129. Duy NguyenOct 1, 2018
  130. Ben PeartOct 2, 2018
  131. 0/7 speed up index load through parallelizationBen Peart, Oct 10, 2018
  132. 1/7 read-cache.c: optimize reading index format v4Ben Peart, Oct 10, 2018
  133. 2/7 read-cache: clean up casting and byte decodingBen Peart, Oct 10, 2018
  134. 3/7 eoie: add End of Index Entry (EOIE) extensionBen Peart, Oct 10, 2018
  135. 4/7 config: add new index.threads config settingBen Peart, Oct 10, 2018
  136. 5/7 read-cache: load cache extensions on a worker threadBen Peart, Oct 10, 2018
  137. 6/7 ieot: add Index Entry Offset Table (IEOT) extensionBen Peart, Oct 10, 2018
  138. 7/7 read-cache: load cache entries on worker threadsBen Peart, Oct 10, 2018
  139. Jeff KingOct 19, 2018
  140. Junio C HamanoOct 22, 2018
  141. Ben PeartOct 22, 2018
  142. Junio C HamanoOct 12, 2018
  143. Duy NguyenOct 14, 2018
  144. Ben PeartOct 15, 2018
  145. 0/3 Avoid confusing messages from new index extensions (Re: [PATCH v8 0/7] speed up index load through parallelization)Jonathan Nieder, Nov 13, 2018
  146. 1/3 eoie: default to not writing EOIE sectionJonathan Nieder, Nov 13, 2018
  147. Junio C HamanoNov 13, 2018
  148. Ben PeartNov 13, 2018
  149. Jonathan NiederNov 13, 2018
  150. Junio C HamanoNov 14, 2018
  151. Jonathan NiederNov 15, 2018
  152. 2/3 ieot: default to not writing IEOT sectionJonathan Nieder, Nov 13, 2018
  153. Jonathan TanNov 13, 2018
  154. Junio C HamanoNov 13, 2018
  155. Jonathan NiederNov 13, 2018
  156. Duy NguyenNov 13, 2018
  157. Jonathan NiederNov 13, 2018
  158. Ben PeartNov 13, 2018
  159. Jonathan NiederNov 13, 2018
  160. Ben PeartNov 13, 2018
  161. Jonathan NiederNov 13, 2018
  162. Ben PeartNov 14, 2018
  163. Jonathan NiederNov 15, 2018
  164. Junio C HamanoNov 14, 2018
  165. 0/5 Avoid confusing messages from new index extensionsJonathan Nieder, Nov 20, 2018
  166. 1/5 eoie: default to not writing EOIE sectionJonathan Nieder, Nov 20, 2018
  167. Ben PeartNov 20, 2018
  168. SZEDER GáborNov 20, 2018
  169. Jeff KingNov 21, 2018
  170. Junio C HamanoNov 22, 2018
  171. Ben PeartNov 20, 2018
  172. 2/5 ieot: default to not writing IEOT sectionJonathan Nieder, Nov 20, 2018
  173. Ben PeartNov 20, 2018
  174. Stefan BellerNov 26, 2018
  175. Ben PeartNov 26, 2018
  176. Stefan BellerNov 26, 2018
  177. Junio C HamanoNov 27, 2018
  178. 3/5 index: do not warn about unrecognized extensionsJonathan Nieder, Nov 20, 2018
  179. 4/5 index: make index.threads=true enable ieot and eoieJonathan Nieder, Nov 20, 2018
  180. Ben PeartNov 20, 2018
  181. 5/5 index: offer advice for unknown index extensionsJonathan Nieder, Nov 20, 2018
  182. Ævar Arnfjörð BjarmasonNov 20, 2018
  183. Ben PeartNov 20, 2018
  184. Junio C HamanoNov 21, 2018
  185. Jonathan NiederNov 21, 2018
  186. Jonathan NiederNov 21, 2018
  187. Junio C HamanoNov 21, 2018
  188. Jonathan NiederNov 21, 2018
  189. Junio C HamanoNov 21, 2018
  190. Junio C HamanoNov 21, 2018
  191. Jonathan NiederNov 21, 2018
  192. Junio C HamanoNov 21, 2018
  193. Jonathan NiederNov 21, 2018
  194. Ævar Arnfjörð BjarmasonNov 21, 2018
  195. 3/3 index: do not warn about unrecognized extensionsJonathan Nieder, Nov 13, 2018
  196. Junio C HamanoNov 13, 2018
  197. Ben PeartNov 13, 2018
  198. Junio C HamanoNov 14, 2018
  199. Ben PeartNov 14, 2018

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.