Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * toast_internals.c
4 : * Functions for internal use by the TOAST system.
5 : *
6 : * Copyright (c) 2000-2025, PostgreSQL Global Development Group
7 : *
8 : * IDENTIFICATION
9 : * src/backend/access/common/toast_internals.c
10 : *
11 : *-------------------------------------------------------------------------
12 : */
13 :
14 : #include "postgres.h"
15 :
16 : #include "access/detoast.h"
17 : #include "access/genam.h"
18 : #include "access/heapam.h"
19 : #include "access/heaptoast.h"
20 : #include "access/table.h"
21 : #include "access/toast_internals.h"
22 : #include "access/xact.h"
23 : #include "catalog/catalog.h"
24 : #include "miscadmin.h"
25 : #include "utils/fmgroids.h"
26 : #include "utils/rel.h"
27 : #include "utils/snapmgr.h"
28 :
29 : static bool toastrel_valueid_exists(Relation toastrel, Oid valueid);
30 : static bool toastid_valueid_exists(Oid toastrelid, Oid valueid);
31 :
32 : /* ----------
33 : * toast_compress_datum -
34 : *
35 : * Create a compressed version of a varlena datum
36 : *
37 : * If we fail (ie, compressed result is actually bigger than original)
38 : * then return NULL. We must not use compressed data if it'd expand
39 : * the tuple!
40 : *
41 : * We use VAR{SIZE,DATA}_ANY so we can handle short varlenas here without
42 : * copying them. But we can't handle external or compressed datums.
43 : * ----------
44 : */
45 : Datum
46 51808 : toast_compress_datum(Datum value, char cmethod)
47 : {
48 51808 : struct varlena *tmp = NULL;
49 : int32 valsize;
50 51808 : ToastCompressionId cmid = TOAST_INVALID_COMPRESSION_ID;
51 :
52 : Assert(!VARATT_IS_EXTERNAL(DatumGetPointer(value)));
53 : Assert(!VARATT_IS_COMPRESSED(DatumGetPointer(value)));
54 :
55 51808 : valsize = VARSIZE_ANY_EXHDR(DatumGetPointer(value));
56 :
57 : /* If the compression method is not valid, use the current default */
58 51808 : if (!CompressionMethodIsValid(cmethod))
59 51724 : cmethod = default_toast_compression;
60 :
61 : /*
62 : * Call appropriate compression routine for the compression method.
63 : */
64 51808 : switch (cmethod)
65 : {
66 51766 : case TOAST_PGLZ_COMPRESSION:
67 51766 : tmp = pglz_compress_datum((const struct varlena *) DatumGetPointer(value));
68 51766 : cmid = TOAST_PGLZ_COMPRESSION_ID;
69 51766 : break;
70 42 : case TOAST_LZ4_COMPRESSION:
71 42 : tmp = lz4_compress_datum((const struct varlena *) DatumGetPointer(value));
72 42 : cmid = TOAST_LZ4_COMPRESSION_ID;
73 42 : break;
74 0 : default:
75 0 : elog(ERROR, "invalid compression method %c", cmethod);
76 : }
77 :
78 51808 : if (tmp == NULL)
79 12726 : return PointerGetDatum(NULL);
80 :
81 : /*
82 : * We recheck the actual size even if compression reports success, because
83 : * it might be satisfied with having saved as little as one byte in the
84 : * compressed data --- which could turn into a net loss once you consider
85 : * header and alignment padding. Worst case, the compressed format might
86 : * require three padding bytes (plus header, which is included in
87 : * VARSIZE(tmp)), whereas the uncompressed format would take only one
88 : * header byte and no padding if the value is short enough. So we insist
89 : * on a savings of more than 2 bytes to ensure we have a gain.
90 : */
91 39082 : if (VARSIZE(tmp) < valsize - 2)
92 : {
93 : /* successful compression */
94 : Assert(cmid != TOAST_INVALID_COMPRESSION_ID);
95 39082 : TOAST_COMPRESS_SET_SIZE_AND_COMPRESS_METHOD(tmp, valsize, cmid);
96 39082 : return PointerGetDatum(tmp);
97 : }
98 : else
99 : {
100 : /* incompressible data */
101 0 : pfree(tmp);
102 0 : return PointerGetDatum(NULL);
103 : }
104 : }
105 :
106 : /* ----------
107 : * toast_save_datum -
108 : *
109 : * Save one single datum into the secondary relation and return
110 : * a Datum reference for it.
111 : *
112 : * rel: the main relation we're working with (not the toast rel!)
113 : * value: datum to be pushed to toast storage
114 : * oldexternal: if not NULL, toast pointer previously representing the datum
115 : * options: options to be passed to heap_insert() for toast rows
116 : * ----------
117 : */
118 : Datum
119 16722 : toast_save_datum(Relation rel, Datum value,
120 : struct varlena *oldexternal, int options)
121 : {
122 : Relation toastrel;
123 : Relation *toastidxs;
124 : TupleDesc toasttupDesc;
125 16722 : CommandId mycid = GetCurrentCommandId(true);
126 : struct varlena *result;
127 : struct varatt_external toast_pointer;
128 16722 : int32 chunk_seq = 0;
129 : char *data_p;
130 : int32 data_todo;
131 16722 : Pointer dval = DatumGetPointer(value);
132 : int num_indexes;
133 : int validIndex;
134 :
135 : Assert(!VARATT_IS_EXTERNAL(dval));
136 :
137 : /*
138 : * Open the toast relation and its indexes. We can use the index to check
139 : * uniqueness of the OID we assign to the toasted item, even though it has
140 : * additional columns besides OID.
141 : */
142 16722 : toastrel = table_open(rel->rd_rel->reltoastrelid, RowExclusiveLock);
143 16722 : toasttupDesc = toastrel->rd_att;
144 :
145 : /* Open all the toast indexes and look for the valid one */
146 16722 : validIndex = toast_open_indexes(toastrel,
147 : RowExclusiveLock,
148 : &toastidxs,
149 : &num_indexes);
150 :
151 : /*
152 : * Get the data pointer and length, and compute va_rawsize and va_extinfo.
153 : *
154 : * va_rawsize is the size of the equivalent fully uncompressed datum, so
155 : * we have to adjust for short headers.
156 : *
157 : * va_extinfo stored the actual size of the data payload in the toast
158 : * records and the compression method in first 2 bits if data is
159 : * compressed.
160 : */
161 16722 : if (VARATT_IS_SHORT(dval))
162 : {
163 6 : data_p = VARDATA_SHORT(dval);
164 6 : data_todo = VARSIZE_SHORT(dval) - VARHDRSZ_SHORT;
165 6 : toast_pointer.va_rawsize = data_todo + VARHDRSZ; /* as if not short */
166 6 : toast_pointer.va_extinfo = data_todo;
167 : }
168 16716 : else if (VARATT_IS_COMPRESSED(dval))
169 : {
170 10266 : data_p = VARDATA(dval);
171 10266 : data_todo = VARSIZE(dval) - VARHDRSZ;
172 : /* rawsize in a compressed datum is just the size of the payload */
173 10266 : toast_pointer.va_rawsize = VARDATA_COMPRESSED_GET_EXTSIZE(dval) + VARHDRSZ;
174 :
175 : /* set external size and compression method */
176 10266 : VARATT_EXTERNAL_SET_SIZE_AND_COMPRESS_METHOD(toast_pointer, data_todo,
177 : VARDATA_COMPRESSED_GET_COMPRESS_METHOD(dval));
178 : /* Assert that the numbers look like it's compressed */
179 : Assert(VARATT_EXTERNAL_IS_COMPRESSED(toast_pointer));
180 : }
181 : else
182 : {
183 6450 : data_p = VARDATA(dval);
184 6450 : data_todo = VARSIZE(dval) - VARHDRSZ;
185 6450 : toast_pointer.va_rawsize = VARSIZE(dval);
186 6450 : toast_pointer.va_extinfo = data_todo;
187 : }
188 :
189 : /*
190 : * Insert the correct table OID into the result TOAST pointer.
191 : *
192 : * Normally this is the actual OID of the target toast table, but during
193 : * table-rewriting operations such as CLUSTER, we have to insert the OID
194 : * of the table's real permanent toast table instead. rd_toastoid is set
195 : * if we have to substitute such an OID.
196 : */
197 16722 : if (OidIsValid(rel->rd_toastoid))
198 598 : toast_pointer.va_toastrelid = rel->rd_toastoid;
199 : else
200 16124 : toast_pointer.va_toastrelid = RelationGetRelid(toastrel);
201 :
202 : /*
203 : * Choose an OID to use as the value ID for this toast value.
204 : *
205 : * Normally we just choose an unused OID within the toast table. But
206 : * during table-rewriting operations where we are preserving an existing
207 : * toast table OID, we want to preserve toast value OIDs too. So, if
208 : * rd_toastoid is set and we had a prior external value from that same
209 : * toast table, re-use its value ID. If we didn't have a prior external
210 : * value (which is a corner case, but possible if the table's attstorage
211 : * options have been changed), we have to pick a value ID that doesn't
212 : * conflict with either new or existing toast value OIDs.
213 : */
214 16722 : if (!OidIsValid(rel->rd_toastoid))
215 : {
216 : /* normal case: just choose an unused OID */
217 16124 : toast_pointer.va_valueid =
218 16124 : GetNewOidWithIndex(toastrel,
219 16124 : RelationGetRelid(toastidxs[validIndex]),
220 : (AttrNumber) 1);
221 : }
222 : else
223 : {
224 : /* rewrite case: check to see if value was in old toast table */
225 598 : toast_pointer.va_valueid = InvalidOid;
226 598 : if (oldexternal != NULL)
227 : {
228 : struct varatt_external old_toast_pointer;
229 :
230 : Assert(VARATT_IS_EXTERNAL_ONDISK(oldexternal));
231 : /* Must copy to access aligned fields */
232 592 : VARATT_EXTERNAL_GET_POINTER(old_toast_pointer, oldexternal);
233 592 : if (old_toast_pointer.va_toastrelid == rel->rd_toastoid)
234 : {
235 : /* This value came from the old toast table; reuse its OID */
236 592 : toast_pointer.va_valueid = old_toast_pointer.va_valueid;
237 :
238 : /*
239 : * There is a corner case here: the table rewrite might have
240 : * to copy both live and recently-dead versions of a row, and
241 : * those versions could easily reference the same toast value.
242 : * When we copy the second or later version of such a row,
243 : * reusing the OID will mean we select an OID that's already
244 : * in the new toast table. Check for that, and if so, just
245 : * fall through without writing the data again.
246 : *
247 : * While annoying and ugly-looking, this is a good thing
248 : * because it ensures that we wind up with only one copy of
249 : * the toast value when there is only one copy in the old
250 : * toast table. Before we detected this case, we'd have made
251 : * multiple copies, wasting space; and what's worse, the
252 : * copies belonging to already-deleted heap tuples would not
253 : * be reclaimed by VACUUM.
254 : */
255 592 : if (toastrel_valueid_exists(toastrel,
256 : toast_pointer.va_valueid))
257 : {
258 : /* Match, so short-circuit the data storage loop below */
259 2 : data_todo = 0;
260 : }
261 : }
262 : }
263 598 : if (toast_pointer.va_valueid == InvalidOid)
264 : {
265 : /*
266 : * new value; must choose an OID that doesn't conflict in either
267 : * old or new toast table
268 : */
269 : do
270 : {
271 6 : toast_pointer.va_valueid =
272 6 : GetNewOidWithIndex(toastrel,
273 6 : RelationGetRelid(toastidxs[validIndex]),
274 : (AttrNumber) 1);
275 6 : } while (toastid_valueid_exists(rel->rd_toastoid,
276 : toast_pointer.va_valueid));
277 : }
278 : }
279 :
280 : /*
281 : * Split up the item into chunks
282 : */
283 78244 : while (data_todo > 0)
284 : {
285 : HeapTuple toasttup;
286 : Datum t_values[3];
287 61522 : bool t_isnull[3] = {0};
288 : union
289 : {
290 : struct varlena hdr;
291 : /* this is to make the union big enough for a chunk: */
292 : char data[TOAST_MAX_CHUNK_SIZE + VARHDRSZ];
293 : /* ensure union is aligned well enough: */
294 : int32 align_it;
295 : } chunk_data;
296 : int32 chunk_size;
297 :
298 61522 : CHECK_FOR_INTERRUPTS();
299 :
300 : /*
301 : * Calculate the size of this chunk
302 : */
303 61522 : chunk_size = Min(TOAST_MAX_CHUNK_SIZE, data_todo);
304 :
305 : /*
306 : * Build a tuple and store it
307 : */
308 61522 : t_values[0] = ObjectIdGetDatum(toast_pointer.va_valueid);
309 61522 : t_values[1] = Int32GetDatum(chunk_seq++);
310 61522 : SET_VARSIZE(&chunk_data, chunk_size + VARHDRSZ);
311 61522 : memcpy(VARDATA(&chunk_data), data_p, chunk_size);
312 61522 : t_values[2] = PointerGetDatum(&chunk_data);
313 :
314 61522 : toasttup = heap_form_tuple(toasttupDesc, t_values, t_isnull);
315 :
316 61522 : heap_insert(toastrel, toasttup, mycid, options, NULL);
317 :
318 : /*
319 : * Create the index entry. We cheat a little here by not using
320 : * FormIndexDatum: this relies on the knowledge that the index columns
321 : * are the same as the initial columns of the table for all the
322 : * indexes. We also cheat by not providing an IndexInfo: this is okay
323 : * for now because btree doesn't need one, but we might have to be
324 : * more honest someday.
325 : *
326 : * Note also that there had better not be any user-created index on
327 : * the TOAST table, since we don't bother to update anything else.
328 : */
329 123044 : for (int i = 0; i < num_indexes; i++)
330 : {
331 : /* Only index relations marked as ready can be updated */
332 61522 : if (toastidxs[i]->rd_index->indisready)
333 61522 : index_insert(toastidxs[i], t_values, t_isnull,
334 : &(toasttup->t_self),
335 : toastrel,
336 61522 : toastidxs[i]->rd_index->indisunique ?
337 : UNIQUE_CHECK_YES : UNIQUE_CHECK_NO,
338 : false, NULL);
339 : }
340 :
341 : /*
342 : * Free memory
343 : */
344 61522 : heap_freetuple(toasttup);
345 :
346 : /*
347 : * Move on to next chunk
348 : */
349 61522 : data_todo -= chunk_size;
350 61522 : data_p += chunk_size;
351 : }
352 :
353 : /*
354 : * Done - close toast relation and its indexes but keep the lock until
355 : * commit, so as a concurrent reindex done directly on the toast relation
356 : * would be able to wait for this transaction.
357 : */
358 16722 : toast_close_indexes(toastidxs, num_indexes, NoLock);
359 16722 : table_close(toastrel, NoLock);
360 :
361 : /*
362 : * Create the TOAST pointer value that we'll return
363 : */
364 16722 : result = (struct varlena *) palloc(TOAST_POINTER_SIZE);
365 16722 : SET_VARTAG_EXTERNAL(result, VARTAG_ONDISK);
366 16722 : memcpy(VARDATA_EXTERNAL(result), &toast_pointer, sizeof(toast_pointer));
367 :
368 16722 : return PointerGetDatum(result);
369 : }
370 :
371 : /* ----------
372 : * toast_delete_datum -
373 : *
374 : * Delete a single external stored value.
375 : * ----------
376 : */
377 : void
378 1388 : toast_delete_datum(Relation rel, Datum value, bool is_speculative)
379 : {
380 1388 : struct varlena *attr = (struct varlena *) DatumGetPointer(value);
381 : struct varatt_external toast_pointer;
382 : Relation toastrel;
383 : Relation *toastidxs;
384 : ScanKeyData toastkey;
385 : SysScanDesc toastscan;
386 : HeapTuple toasttup;
387 : int num_indexes;
388 : int validIndex;
389 :
390 1388 : if (!VARATT_IS_EXTERNAL_ONDISK(attr))
391 0 : return;
392 :
393 : /* Must copy to access aligned fields */
394 1388 : VARATT_EXTERNAL_GET_POINTER(toast_pointer, attr);
395 :
396 : /*
397 : * Open the toast relation and its indexes
398 : */
399 1388 : toastrel = table_open(toast_pointer.va_toastrelid, RowExclusiveLock);
400 :
401 : /* Fetch valid relation used for process */
402 1388 : validIndex = toast_open_indexes(toastrel,
403 : RowExclusiveLock,
404 : &toastidxs,
405 : &num_indexes);
406 :
407 : /*
408 : * Setup a scan key to find chunks with matching va_valueid
409 : */
410 1388 : ScanKeyInit(&toastkey,
411 : (AttrNumber) 1,
412 : BTEqualStrategyNumber, F_OIDEQ,
413 : ObjectIdGetDatum(toast_pointer.va_valueid));
414 :
415 : /*
416 : * Find all the chunks. (We don't actually care whether we see them in
417 : * sequence or not, but since we've already locked the index we might as
418 : * well use systable_beginscan_ordered.)
419 : */
420 1388 : toastscan = systable_beginscan_ordered(toastrel, toastidxs[validIndex],
421 : get_toast_snapshot(), 1, &toastkey);
422 6508 : while ((toasttup = systable_getnext_ordered(toastscan, ForwardScanDirection)) != NULL)
423 : {
424 : /*
425 : * Have a chunk, delete it
426 : */
427 5120 : if (is_speculative)
428 10 : heap_abort_speculative(toastrel, &toasttup->t_self);
429 : else
430 5110 : simple_heap_delete(toastrel, &toasttup->t_self);
431 : }
432 :
433 : /*
434 : * End scan and close relations but keep the lock until commit, so as a
435 : * concurrent reindex done directly on the toast relation would be able to
436 : * wait for this transaction.
437 : */
438 1388 : systable_endscan_ordered(toastscan);
439 1388 : toast_close_indexes(toastidxs, num_indexes, NoLock);
440 1388 : table_close(toastrel, NoLock);
441 : }
442 :
443 : /* ----------
444 : * toastrel_valueid_exists -
445 : *
446 : * Test whether a toast value with the given ID exists in the toast relation.
447 : * For safety, we consider a value to exist if there are either live or dead
448 : * toast rows with that ID; see notes for GetNewOidWithIndex().
449 : * ----------
450 : */
451 : static bool
452 598 : toastrel_valueid_exists(Relation toastrel, Oid valueid)
453 : {
454 598 : bool result = false;
455 : ScanKeyData toastkey;
456 : SysScanDesc toastscan;
457 : int num_indexes;
458 : int validIndex;
459 : Relation *toastidxs;
460 :
461 : /* Fetch a valid index relation */
462 598 : validIndex = toast_open_indexes(toastrel,
463 : RowExclusiveLock,
464 : &toastidxs,
465 : &num_indexes);
466 :
467 : /*
468 : * Setup a scan key to find chunks with matching va_valueid
469 : */
470 598 : ScanKeyInit(&toastkey,
471 : (AttrNumber) 1,
472 : BTEqualStrategyNumber, F_OIDEQ,
473 : ObjectIdGetDatum(valueid));
474 :
475 : /*
476 : * Is there any such chunk?
477 : */
478 598 : toastscan = systable_beginscan(toastrel,
479 598 : RelationGetRelid(toastidxs[validIndex]),
480 : true, SnapshotAny, 1, &toastkey);
481 :
482 598 : if (systable_getnext(toastscan) != NULL)
483 2 : result = true;
484 :
485 598 : systable_endscan(toastscan);
486 :
487 : /* Clean up */
488 598 : toast_close_indexes(toastidxs, num_indexes, RowExclusiveLock);
489 :
490 598 : return result;
491 : }
492 :
493 : /* ----------
494 : * toastid_valueid_exists -
495 : *
496 : * As above, but work from toast rel's OID not an open relation
497 : * ----------
498 : */
499 : static bool
500 6 : toastid_valueid_exists(Oid toastrelid, Oid valueid)
501 : {
502 : bool result;
503 : Relation toastrel;
504 :
505 6 : toastrel = table_open(toastrelid, AccessShareLock);
506 :
507 6 : result = toastrel_valueid_exists(toastrel, valueid);
508 :
509 6 : table_close(toastrel, AccessShareLock);
510 :
511 6 : return result;
512 : }
513 :
514 : /* ----------
515 : * toast_get_valid_index
516 : *
517 : * Get OID of valid index associated to given toast relation. A toast
518 : * relation can have only one valid index at the same time.
519 : */
520 : Oid
521 984 : toast_get_valid_index(Oid toastoid, LOCKMODE lock)
522 : {
523 : int num_indexes;
524 : int validIndex;
525 : Oid validIndexOid;
526 : Relation *toastidxs;
527 : Relation toastrel;
528 :
529 : /* Open the toast relation */
530 984 : toastrel = table_open(toastoid, lock);
531 :
532 : /* Look for the valid index of the toast relation */
533 984 : validIndex = toast_open_indexes(toastrel,
534 : lock,
535 : &toastidxs,
536 : &num_indexes);
537 984 : validIndexOid = RelationGetRelid(toastidxs[validIndex]);
538 :
539 : /* Close the toast relation and all its indexes */
540 984 : toast_close_indexes(toastidxs, num_indexes, NoLock);
541 984 : table_close(toastrel, NoLock);
542 :
543 984 : return validIndexOid;
544 : }
545 :
546 : /* ----------
547 : * toast_open_indexes
548 : *
549 : * Get an array of the indexes associated to the given toast relation
550 : * and return as well the position of the valid index used by the toast
551 : * relation in this array. It is the responsibility of the caller of this
552 : * function to close the indexes as well as free them.
553 : */
554 : int
555 44468 : toast_open_indexes(Relation toastrel,
556 : LOCKMODE lock,
557 : Relation **toastidxs,
558 : int *num_indexes)
559 : {
560 44468 : int i = 0;
561 44468 : int res = 0;
562 44468 : bool found = false;
563 : List *indexlist;
564 : ListCell *lc;
565 :
566 : /* Get index list of the toast relation */
567 44468 : indexlist = RelationGetIndexList(toastrel);
568 : Assert(indexlist != NIL);
569 :
570 44468 : *num_indexes = list_length(indexlist);
571 :
572 : /* Open all the index relations */
573 44468 : *toastidxs = (Relation *) palloc(*num_indexes * sizeof(Relation));
574 88936 : foreach(lc, indexlist)
575 44468 : (*toastidxs)[i++] = index_open(lfirst_oid(lc), lock);
576 :
577 : /* Fetch the first valid index in list */
578 44468 : for (i = 0; i < *num_indexes; i++)
579 : {
580 44468 : Relation toastidx = (*toastidxs)[i];
581 :
582 44468 : if (toastidx->rd_index->indisvalid)
583 : {
584 44468 : res = i;
585 44468 : found = true;
586 44468 : break;
587 : }
588 : }
589 :
590 : /*
591 : * Free index list, not necessary anymore as relations are opened and a
592 : * valid index has been found.
593 : */
594 44468 : list_free(indexlist);
595 :
596 : /*
597 : * The toast relation should have one valid index, so something is going
598 : * wrong if there is nothing.
599 : */
600 44468 : if (!found)
601 0 : elog(ERROR, "no valid index found for toast relation with Oid %u",
602 : RelationGetRelid(toastrel));
603 :
604 44468 : return res;
605 : }
606 :
607 : /* ----------
608 : * toast_close_indexes
609 : *
610 : * Close an array of indexes for a toast relation and free it. This should
611 : * be called for a set of indexes opened previously with toast_open_indexes.
612 : */
613 : void
614 44462 : toast_close_indexes(Relation *toastidxs, int num_indexes, LOCKMODE lock)
615 : {
616 : int i;
617 :
618 : /* Close relations and clean up things */
619 88924 : for (i = 0; i < num_indexes; i++)
620 44462 : index_close(toastidxs[i], lock);
621 44462 : pfree(toastidxs);
622 44462 : }
623 :
624 : /* ----------
625 : * get_toast_snapshot
626 : *
627 : * Return the TOAST snapshot. Detoasting *must* happen in the same
628 : * transaction that originally fetched the toast pointer.
629 : */
630 : Snapshot
631 48266 : get_toast_snapshot(void)
632 : {
633 : /*
634 : * We cannot directly check that detoasting happens in the same
635 : * transaction that originally fetched the toast pointer, but at least
636 : * check that the session has some active snapshots. It might not if, for
637 : * example, a procedure fetches a toasted value into a local variable,
638 : * commits, and then tries to detoast the value. Such coding is unsafe,
639 : * because once we commit there is nothing to prevent the toast data from
640 : * being deleted. (This is not very much protection, because in many
641 : * scenarios the procedure would have already created a new transaction
642 : * snapshot, preventing us from detecting the problem. But it's better
643 : * than nothing.)
644 : */
645 48266 : if (!HaveRegisteredOrActiveSnapshot())
646 0 : elog(ERROR, "cannot fetch toast data without an active snapshot");
647 :
648 48266 : return &SnapshotToastData;
649 : }
|