github.com/cockroachdb/pebble@v0.0.0-20231214172447-ab4952c5f87b/tool/manifest.go (about)

     1  // Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
     2  // of this source code is governed by a BSD-style license that can be found in
     3  // the LICENSE file.
     4  
     5  package tool
     6  
     7  import (
     8  	"cmp"
     9  	"fmt"
    10  	"io"
    11  	"slices"
    12  	"time"
    13  
    14  	"github.com/cockroachdb/pebble"
    15  	"github.com/cockroachdb/pebble/internal/base"
    16  	"github.com/cockroachdb/pebble/internal/humanize"
    17  	"github.com/cockroachdb/pebble/internal/manifest"
    18  	"github.com/cockroachdb/pebble/record"
    19  	"github.com/cockroachdb/pebble/sstable"
    20  	"github.com/spf13/cobra"
    21  )
    22  
    23  // manifestT implements manifest-level tools, including both configuration
    24  // state and the commands themselves.
    25  type manifestT struct {
    26  	Root      *cobra.Command
    27  	Dump      *cobra.Command
    28  	Summarize *cobra.Command
    29  	Check     *cobra.Command
    30  
    31  	opts      *pebble.Options
    32  	comparers sstable.Comparers
    33  	fmtKey    keyFormatter
    34  	verbose   bool
    35  
    36  	filterStart key
    37  	filterEnd   key
    38  
    39  	summarizeDur time.Duration
    40  }
    41  
    42  func newManifest(opts *pebble.Options, comparers sstable.Comparers) *manifestT {
    43  	m := &manifestT{
    44  		opts:         opts,
    45  		comparers:    comparers,
    46  		summarizeDur: time.Hour,
    47  	}
    48  	m.fmtKey.mustSet("quoted")
    49  
    50  	m.Root = &cobra.Command{
    51  		Use:   "manifest",
    52  		Short: "manifest introspection tools",
    53  	}
    54  
    55  	// Add dump command
    56  	m.Dump = &cobra.Command{
    57  		Use:   "dump <manifest-files>",
    58  		Short: "print manifest contents",
    59  		Long: `
    60  Print the contents of the MANIFEST files.
    61  `,
    62  		Args: cobra.MinimumNArgs(1),
    63  		Run:  m.runDump,
    64  	}
    65  	m.Dump.Flags().Var(&m.fmtKey, "key", "key formatter")
    66  	m.Dump.Flags().Var(&m.filterStart, "filter-start", "start key filters out all version edits that only reference sstables containing keys strictly before the given key")
    67  	m.Dump.Flags().Var(&m.filterEnd, "filter-end", "end key filters out all version edits that only reference sstables containing keys at or strictly after the given key")
    68  	m.Root.AddCommand(m.Dump)
    69  	m.Root.PersistentFlags().BoolVarP(&m.verbose, "verbose", "v", false, "verbose output")
    70  
    71  	// Add summarize command
    72  	m.Summarize = &cobra.Command{
    73  		Use:   "summarize <manifest-files>",
    74  		Short: "summarize manifest contents",
    75  		Long: `
    76  Summarize the edits to the MANIFEST files over time.
    77  `,
    78  		Args: cobra.MinimumNArgs(1),
    79  		Run:  m.runSummarize,
    80  	}
    81  	m.Root.AddCommand(m.Summarize)
    82  	m.Summarize.Flags().DurationVar(
    83  		&m.summarizeDur, "dur", time.Hour, "bucket duration as a Go duration string (eg, '1h', '15m')")
    84  
    85  	// Add check command
    86  	m.Check = &cobra.Command{
    87  		Use:   "check <manifest-files>",
    88  		Short: "check manifest contents",
    89  		Long: `
    90  Check the contents of the MANIFEST files.
    91  `,
    92  		Args: cobra.MinimumNArgs(1),
    93  		Run:  m.runCheck,
    94  	}
    95  	m.Root.AddCommand(m.Check)
    96  	m.Check.Flags().Var(
    97  		&m.fmtKey, "key", "key formatter")
    98  
    99  	return m
   100  }
   101  
   102  func (m *manifestT) printLevels(cmp base.Compare, stdout io.Writer, v *manifest.Version) {
   103  	for level := range v.Levels {
   104  		if level == 0 && len(v.L0SublevelFiles) > 0 && !v.Levels[level].Empty() {
   105  			for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- {
   106  				fmt.Fprintf(stdout, "--- L0.%d ---\n", sublevel)
   107  				v.L0SublevelFiles[sublevel].Each(func(f *manifest.FileMetadata) {
   108  					if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) {
   109  						return
   110  					}
   111  					fmt.Fprintf(stdout, "  %s:%d", f.FileNum, f.Size)
   112  					formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum)
   113  					formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest)
   114  					if f.Virtual {
   115  						fmt.Fprintf(stdout, "(virtual:backingNum=%s)", f.FileBacking.DiskFileNum)
   116  					}
   117  					fmt.Fprintf(stdout, "\n")
   118  				})
   119  			}
   120  			continue
   121  		}
   122  		fmt.Fprintf(stdout, "--- L%d ---\n", level)
   123  		iter := v.Levels[level].Iter()
   124  		for f := iter.First(); f != nil; f = iter.Next() {
   125  			if !anyOverlapFile(cmp, f, m.filterStart, m.filterEnd) {
   126  				continue
   127  			}
   128  			fmt.Fprintf(stdout, "  %s:%d", f.FileNum, f.Size)
   129  			formatSeqNumRange(stdout, f.SmallestSeqNum, f.LargestSeqNum)
   130  			formatKeyRange(stdout, m.fmtKey, &f.Smallest, &f.Largest)
   131  			if f.Virtual {
   132  				fmt.Fprintf(stdout, "(virtual:backingNum=%s)", f.FileBacking.DiskFileNum)
   133  			}
   134  			fmt.Fprintf(stdout, "\n")
   135  		}
   136  	}
   137  }
   138  
   139  func (m *manifestT) runDump(cmd *cobra.Command, args []string) {
   140  	stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
   141  	for _, arg := range args {
   142  		func() {
   143  			f, err := m.opts.FS.Open(arg)
   144  			if err != nil {
   145  				fmt.Fprintf(stderr, "%s\n", err)
   146  				return
   147  			}
   148  			defer f.Close()
   149  
   150  			fmt.Fprintf(stdout, "%s\n", arg)
   151  
   152  			var bve manifest.BulkVersionEdit
   153  			bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata)
   154  			var comparer *base.Comparer
   155  			var editIdx int
   156  			rr := record.NewReader(f, 0 /* logNum */)
   157  			for {
   158  				offset := rr.Offset()
   159  				r, err := rr.Next()
   160  				if err != nil {
   161  					fmt.Fprintf(stdout, "%s\n", err)
   162  					break
   163  				}
   164  
   165  				var ve manifest.VersionEdit
   166  				err = ve.Decode(r)
   167  				if err != nil {
   168  					fmt.Fprintf(stdout, "%s\n", err)
   169  					break
   170  				}
   171  				if err := bve.Accumulate(&ve); err != nil {
   172  					fmt.Fprintf(stdout, "%s\n", err)
   173  					break
   174  				}
   175  
   176  				if comparer != nil && !anyOverlap(comparer.Compare, &ve, m.filterStart, m.filterEnd) {
   177  					continue
   178  				}
   179  
   180  				empty := true
   181  				fmt.Fprintf(stdout, "%d/%d\n", offset, editIdx)
   182  				if ve.ComparerName != "" {
   183  					empty = false
   184  					fmt.Fprintf(stdout, "  comparer:     %s", ve.ComparerName)
   185  					comparer = m.comparers[ve.ComparerName]
   186  					if comparer == nil {
   187  						fmt.Fprintf(stdout, " (unknown)")
   188  					}
   189  					fmt.Fprintf(stdout, "\n")
   190  					m.fmtKey.setForComparer(ve.ComparerName, m.comparers)
   191  				}
   192  				if ve.MinUnflushedLogNum != 0 {
   193  					empty = false
   194  					fmt.Fprintf(stdout, "  log-num:       %d\n", ve.MinUnflushedLogNum)
   195  				}
   196  				if ve.ObsoletePrevLogNum != 0 {
   197  					empty = false
   198  					fmt.Fprintf(stdout, "  prev-log-num:  %d\n", ve.ObsoletePrevLogNum)
   199  				}
   200  				if ve.NextFileNum != 0 {
   201  					empty = false
   202  					fmt.Fprintf(stdout, "  next-file-num: %d\n", ve.NextFileNum)
   203  				}
   204  				if ve.LastSeqNum != 0 {
   205  					empty = false
   206  					fmt.Fprintf(stdout, "  last-seq-num:  %d\n", ve.LastSeqNum)
   207  				}
   208  				entries := make([]manifest.DeletedFileEntry, 0, len(ve.DeletedFiles))
   209  				for df := range ve.DeletedFiles {
   210  					empty = false
   211  					entries = append(entries, df)
   212  				}
   213  				slices.SortFunc(entries, func(a, b manifest.DeletedFileEntry) int {
   214  					if v := cmp.Compare(a.Level, b.Level); v != 0 {
   215  						return v
   216  					}
   217  					return cmp.Compare(a.FileNum, b.FileNum)
   218  				})
   219  				for _, df := range entries {
   220  					fmt.Fprintf(stdout, "  deleted:       L%d %s\n", df.Level, df.FileNum)
   221  				}
   222  				for _, nf := range ve.NewFiles {
   223  					empty = false
   224  					fmt.Fprintf(stdout, "  added:         L%d %s:%d",
   225  						nf.Level, nf.Meta.FileNum, nf.Meta.Size)
   226  					formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum)
   227  					formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest)
   228  					if nf.Meta.CreationTime != 0 {
   229  						fmt.Fprintf(stdout, " (%s)",
   230  							time.Unix(nf.Meta.CreationTime, 0).UTC().Format(time.RFC3339))
   231  					}
   232  					fmt.Fprintf(stdout, "\n")
   233  				}
   234  				if empty {
   235  					// NB: An empty version edit can happen if we log a version edit with
   236  					// a zero field. RocksDB does this with a version edit that contains
   237  					// `LogNum == 0`.
   238  					fmt.Fprintf(stdout, "  <empty>\n")
   239  				}
   240  				editIdx++
   241  			}
   242  
   243  			if comparer != nil {
   244  				v, err := bve.Apply(
   245  					nil /* version */, comparer.Compare, m.fmtKey.fn, 0,
   246  					m.opts.Experimental.ReadCompactionRate,
   247  					nil /* zombies */, manifest.AllowSplitUserKeys,
   248  				)
   249  				if err != nil {
   250  					fmt.Fprintf(stdout, "%s\n", err)
   251  					return
   252  				}
   253  				m.printLevels(comparer.Compare, stdout, v)
   254  			}
   255  		}()
   256  	}
   257  }
   258  
   259  func anyOverlap(cmp base.Compare, ve *manifest.VersionEdit, start, end key) bool {
   260  	if start == nil && end == nil {
   261  		return true
   262  	}
   263  	for _, df := range ve.DeletedFiles {
   264  		if anyOverlapFile(cmp, df, start, end) {
   265  			return true
   266  		}
   267  	}
   268  	for _, nf := range ve.NewFiles {
   269  		if anyOverlapFile(cmp, nf.Meta, start, end) {
   270  			return true
   271  		}
   272  	}
   273  	return false
   274  }
   275  
   276  func anyOverlapFile(cmp base.Compare, f *manifest.FileMetadata, start, end key) bool {
   277  	if f == nil {
   278  		return true
   279  	}
   280  	if start != nil {
   281  		if v := cmp(f.Largest.UserKey, start); v < 0 {
   282  			return false
   283  		} else if f.Largest.IsExclusiveSentinel() && v == 0 {
   284  			return false
   285  		}
   286  	}
   287  	if end != nil && cmp(f.Smallest.UserKey, end) >= 0 {
   288  		return false
   289  	}
   290  	return true
   291  }
   292  
   293  func (m *manifestT) runSummarize(cmd *cobra.Command, args []string) {
   294  	for _, arg := range args {
   295  		err := m.runSummarizeOne(cmd.OutOrStdout(), arg)
   296  		if err != nil {
   297  			fmt.Fprintf(cmd.OutOrStderr(), "%s\n", err)
   298  		}
   299  	}
   300  }
   301  
   302  func (m *manifestT) runSummarizeOne(stdout io.Writer, arg string) error {
   303  	f, err := m.opts.FS.Open(arg)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	defer f.Close()
   308  	fmt.Fprintf(stdout, "%s\n", arg)
   309  
   310  	type summaryBucket struct {
   311  		bytesAdded      [manifest.NumLevels]uint64
   312  		bytesCompactOut [manifest.NumLevels]uint64
   313  	}
   314  	var (
   315  		bve           manifest.BulkVersionEdit
   316  		newestOverall time.Time
   317  		oldestOverall time.Time // oldest after initial version edit
   318  		buckets       = map[time.Time]*summaryBucket{}
   319  		metadatas     = map[base.FileNum]*manifest.FileMetadata{}
   320  	)
   321  	bve.AddedByFileNum = make(map[base.FileNum]*manifest.FileMetadata)
   322  	rr := record.NewReader(f, 0 /* logNum */)
   323  	for i := 0; ; i++ {
   324  		r, err := rr.Next()
   325  		if err == io.EOF {
   326  			break
   327  		} else if err != nil {
   328  			return err
   329  		}
   330  
   331  		var ve manifest.VersionEdit
   332  		err = ve.Decode(r)
   333  		if err != nil {
   334  			return err
   335  		}
   336  		if err := bve.Accumulate(&ve); err != nil {
   337  			return err
   338  		}
   339  
   340  		veNewest, veOldest := newestOverall, newestOverall
   341  		for _, nf := range ve.NewFiles {
   342  			_, seen := metadatas[nf.Meta.FileNum]
   343  			metadatas[nf.Meta.FileNum] = nf.Meta
   344  			if nf.Meta.CreationTime == 0 {
   345  				continue
   346  			}
   347  
   348  			t := time.Unix(nf.Meta.CreationTime, 0).UTC()
   349  			if veNewest.Before(t) {
   350  				veNewest = t
   351  			}
   352  			// Only update the oldest if we haven't already seen this
   353  			// file; it might've been moved in which case the sstable's
   354  			// creation time is from when it was originally created.
   355  			if veOldest.After(t) && !seen {
   356  				veOldest = t
   357  			}
   358  		}
   359  		// Ratchet up the most recent timestamp we've seen.
   360  		if newestOverall.Before(veNewest) {
   361  			newestOverall = veNewest
   362  		}
   363  
   364  		if i == 0 || newestOverall.IsZero() {
   365  			continue
   366  		}
   367  		// Update oldestOverall once, when we encounter the first version edit
   368  		// at index >= 1. It should be approximately the start time of the
   369  		// manifest.
   370  		if !newestOverall.IsZero() && oldestOverall.IsZero() {
   371  			oldestOverall = newestOverall
   372  		}
   373  
   374  		bucketKey := newestOverall.Truncate(m.summarizeDur)
   375  		b := buckets[bucketKey]
   376  		if b == nil {
   377  			b = &summaryBucket{}
   378  			buckets[bucketKey] = b
   379  		}
   380  
   381  		// Increase `bytesAdded` for any version edits that only add files.
   382  		// These are either flushes or ingests.
   383  		if len(ve.NewFiles) > 0 && len(ve.DeletedFiles) == 0 {
   384  			for _, nf := range ve.NewFiles {
   385  				b.bytesAdded[nf.Level] += nf.Meta.Size
   386  			}
   387  			continue
   388  		}
   389  
   390  		// Increase `bytesCompactOut` for the input level of any compactions
   391  		// that remove bytes from a level (excluding intra-L0 compactions).
   392  		// compactions.
   393  		destLevel := -1
   394  		if len(ve.NewFiles) > 0 {
   395  			destLevel = ve.NewFiles[0].Level
   396  		}
   397  		for dfe := range ve.DeletedFiles {
   398  			if dfe.Level != destLevel {
   399  				b.bytesCompactOut[dfe.Level] += metadatas[dfe.FileNum].Size
   400  			}
   401  		}
   402  	}
   403  
   404  	formatUint64 := func(v uint64, _ time.Duration) string {
   405  		if v == 0 {
   406  			return "."
   407  		}
   408  		return humanize.Bytes.Uint64(v).String()
   409  	}
   410  	formatRate := func(v uint64, dur time.Duration) string {
   411  		if v == 0 {
   412  			return "."
   413  		}
   414  		secs := dur.Seconds()
   415  		if secs == 0 {
   416  			secs = 1
   417  		}
   418  		return humanize.Bytes.Uint64(uint64(float64(v)/secs)).String() + "/s"
   419  	}
   420  
   421  	if newestOverall.IsZero() {
   422  		fmt.Fprintf(stdout, "(no timestamps)\n")
   423  	} else {
   424  		// NB: bt begins unaligned with the bucket duration (m.summarizeDur),
   425  		// but after the first bucket will always be aligned.
   426  		for bi, bt := 0, oldestOverall; !bt.After(newestOverall); bi, bt = bi+1, bt.Truncate(m.summarizeDur).Add(m.summarizeDur) {
   427  			// Truncate the start time to calculate the bucket key, and
   428  			// retrieve the appropriate bucket.
   429  			bk := bt.Truncate(m.summarizeDur)
   430  			var bucket summaryBucket
   431  			if buckets[bk] != nil {
   432  				bucket = *buckets[bk]
   433  			}
   434  
   435  			if bi%10 == 0 {
   436  				fmt.Fprintf(stdout, "                     ")
   437  				fmt.Fprintf(stdout, "_______L0_______L1_______L2_______L3_______L4_______L5_______L6_____TOTAL\n")
   438  			}
   439  			fmt.Fprintf(stdout, "%s\n", bt.Format(time.RFC3339))
   440  
   441  			// Compute the bucket duration. It may < `m.summarizeDur` if this is
   442  			// the first or last bucket.
   443  			bucketEnd := bt.Truncate(m.summarizeDur).Add(m.summarizeDur)
   444  			if bucketEnd.After(newestOverall) {
   445  				bucketEnd = newestOverall
   446  			}
   447  			dur := bucketEnd.Sub(bt)
   448  
   449  			stats := []struct {
   450  				label  string
   451  				format func(uint64, time.Duration) string
   452  				vals   [manifest.NumLevels]uint64
   453  			}{
   454  				{"Ingest+Flush", formatUint64, bucket.bytesAdded},
   455  				{"Ingest+Flush", formatRate, bucket.bytesAdded},
   456  				{"Compact (out)", formatUint64, bucket.bytesCompactOut},
   457  				{"Compact (out)", formatRate, bucket.bytesCompactOut},
   458  			}
   459  			for _, stat := range stats {
   460  				var sum uint64
   461  				for _, v := range stat.vals {
   462  					sum += v
   463  				}
   464  				fmt.Fprintf(stdout, "%20s   %8s %8s %8s %8s %8s %8s %8s %8s\n",
   465  					stat.label,
   466  					stat.format(stat.vals[0], dur),
   467  					stat.format(stat.vals[1], dur),
   468  					stat.format(stat.vals[2], dur),
   469  					stat.format(stat.vals[3], dur),
   470  					stat.format(stat.vals[4], dur),
   471  					stat.format(stat.vals[5], dur),
   472  					stat.format(stat.vals[6], dur),
   473  					stat.format(sum, dur))
   474  			}
   475  		}
   476  		fmt.Fprintf(stdout, "%s\n", newestOverall.Format(time.RFC3339))
   477  	}
   478  
   479  	dur := newestOverall.Sub(oldestOverall)
   480  	fmt.Fprintf(stdout, "---\n")
   481  	fmt.Fprintf(stdout, "Estimated start time: %s\n", oldestOverall.Format(time.RFC3339))
   482  	fmt.Fprintf(stdout, "Estimated end time:   %s\n", newestOverall.Format(time.RFC3339))
   483  	fmt.Fprintf(stdout, "Estimated duration:   %s\n", dur.String())
   484  
   485  	return nil
   486  }
   487  
   488  func (m *manifestT) runCheck(cmd *cobra.Command, args []string) {
   489  	stdout, stderr := cmd.OutOrStdout(), cmd.OutOrStderr()
   490  	ok := true
   491  	for _, arg := range args {
   492  		func() {
   493  			f, err := m.opts.FS.Open(arg)
   494  			if err != nil {
   495  				fmt.Fprintf(stderr, "%s\n", err)
   496  				ok = false
   497  				return
   498  			}
   499  			defer f.Close()
   500  
   501  			var v *manifest.Version
   502  			var cmp *base.Comparer
   503  			rr := record.NewReader(f, 0 /* logNum */)
   504  			// Contains the FileMetadata needed by BulkVersionEdit.Apply.
   505  			// It accumulates the additions since later edits contain
   506  			// deletions of earlier added files.
   507  			addedByFileNum := make(map[base.FileNum]*manifest.FileMetadata)
   508  			for {
   509  				offset := rr.Offset()
   510  				r, err := rr.Next()
   511  				if err != nil {
   512  					if err == io.EOF {
   513  						break
   514  					}
   515  					fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err)
   516  					ok = false
   517  					break
   518  				}
   519  
   520  				var ve manifest.VersionEdit
   521  				err = ve.Decode(r)
   522  				if err != nil {
   523  					fmt.Fprintf(stdout, "%s: offset: %d err: %s\n", arg, offset, err)
   524  					ok = false
   525  					break
   526  				}
   527  				var bve manifest.BulkVersionEdit
   528  				bve.AddedByFileNum = addedByFileNum
   529  				if err := bve.Accumulate(&ve); err != nil {
   530  					fmt.Fprintf(stderr, "%s\n", err)
   531  					ok = false
   532  					return
   533  				}
   534  
   535  				empty := true
   536  				if ve.ComparerName != "" {
   537  					empty = false
   538  					cmp = m.comparers[ve.ComparerName]
   539  					if cmp == nil {
   540  						fmt.Fprintf(stdout, "%s: offset: %d comparer %s not found",
   541  							arg, offset, ve.ComparerName)
   542  						ok = false
   543  						break
   544  					}
   545  					m.fmtKey.setForComparer(ve.ComparerName, m.comparers)
   546  				}
   547  				empty = empty && ve.MinUnflushedLogNum == 0 && ve.ObsoletePrevLogNum == 0 &&
   548  					ve.LastSeqNum == 0 && len(ve.DeletedFiles) == 0 &&
   549  					len(ve.NewFiles) == 0
   550  				if empty {
   551  					continue
   552  				}
   553  				// TODO(sbhola): add option to Apply that reports all errors instead of
   554  				// one error.
   555  				newv, err := bve.Apply(v, cmp.Compare, m.fmtKey.fn, 0, m.opts.Experimental.ReadCompactionRate, nil /* zombies */, manifest.AllowSplitUserKeys)
   556  				if err != nil {
   557  					fmt.Fprintf(stdout, "%s: offset: %d err: %s\n",
   558  						arg, offset, err)
   559  					fmt.Fprintf(stdout, "Version state before failed Apply\n")
   560  					m.printLevels(cmp.Compare, stdout, v)
   561  					fmt.Fprintf(stdout, "Version edit that failed\n")
   562  					for df := range ve.DeletedFiles {
   563  						fmt.Fprintf(stdout, "  deleted: L%d %s\n", df.Level, df.FileNum)
   564  					}
   565  					for _, nf := range ve.NewFiles {
   566  						fmt.Fprintf(stdout, "  added: L%d %s:%d",
   567  							nf.Level, nf.Meta.FileNum, nf.Meta.Size)
   568  						formatSeqNumRange(stdout, nf.Meta.SmallestSeqNum, nf.Meta.LargestSeqNum)
   569  						formatKeyRange(stdout, m.fmtKey, &nf.Meta.Smallest, &nf.Meta.Largest)
   570  						fmt.Fprintf(stdout, "\n")
   571  					}
   572  					ok = false
   573  					break
   574  				}
   575  				v = newv
   576  			}
   577  		}()
   578  	}
   579  	if ok {
   580  		fmt.Fprintf(stdout, "OK\n")
   581  	}
   582  }