Debugger.Memory

The Debugger API 可以帮助工具以各种方式观察被调试程序的内存使用情况。

  • 它可以为每个新对象标记分配它的 JavaScript 调用栈。

  • 它可以记录所有对象分配,生成一个 JavaScript 调用栈流,这些栈表示分配发生的时刻。

  • 它可以计算被调试程序所属项的普查,以各种方式对项进行分类,并产生项计数。

如果 dbg 是一个 Debugger 实例,则 dbg.memory 的方法和访问器属性控制 dbg 如何观察其被调试程序的内存使用情况。 dbg.memory 对象是 Debugger.Memory 的一个实例;其继承的访问器和方法在下面描述。

分配位置跟踪

如果满足以下条件,JavaScript 引擎会为每个新对象标记分配它的调用栈:

给定一个 Debugger.Object 实例 dobj,它引用某个对象,dobj.allocationSite 返回一个 保存的调用栈,指示 dobj 的引用在哪里分配。

分配日志记录

如果 dbg 是一个 Debugger 实例,并且 dbg.memory.trackingAllocationSites 设置为 true,则 JavaScript 引擎会记录 dbg 的被调试程序代码分配的每个对象。您可以通过调用 dbg.memory.drainAllocationsLog 来检索当前日志。您可以通过设置 dbg.memory.maxAllocationsLogLength 来控制日志大小的限制。

普查

普查是对属于特定 Debugger 的被调试程序的所有可达内存项的图进行完整遍历。它会根据各种标准对这些项进行计数。如果 dbg 是一个 Debugger 实例,您可以调用 dbg.memory.takeCensus 对其被调试程序拥有的资源进行普查。

The Debugger.Memory.prototype 对象的访问器属性

如果 dbg 是一个 Debugger 实例,则 <i>dbg</i>.memory 是一个 Debugger.Memory 实例,它从其原型继承以下访问器属性

trackingAllocationSites

一个布尔值,指示此 Debugger.Memory 实例是否在分配每个对象时捕获 JavaScript 执行栈。此访问器属性既有 getter 也有 setter:为其赋值可以启用或禁用分配位置跟踪。读取访问器会在 Debugger 捕获对象分配的栈时生成 true,否则生成 false。在新的 Debugger 中,分配位置跟踪最初是被禁用的。

赋值可能会失败:如果 Debugger 无法跟踪分配位置,则会抛出一个 Error 实例。

您可以使用 Debugger.Object.prototype.allocationSite 访问器属性检索给定对象的分配位置。

allocationSamplingProbability

一个介于 0 和 1 之间的数字,表示应将每个新分配写入分配日志的概率。0 等效于“从不”,1 等效于“总是”,而 .05 则表示“二十分之一”。

默认值为 1,或记录每个分配。

请注意,在多个 Debugger 实例观察全局作用域内相同分配的情况下,将使用所有 DebuggerallocationSamplingProbability 的最大值。

maxAllocationsLogLength

一次在分配日志中累积的分配位置的最大数量。此访问器既可以被获取也可以被存储。其默认值为 5000

allocationsLogOverflowed

如果自上次调用 [drainAllocationsLog][#drain-alloc-log] 以来,分配数量超过 [maxAllocationsLogLength][#max-alloc-log],并且某些数据已丢失,则返回 true。否则返回 false

Debugger.Memory 处理程序函数

类似于 Debugger 的处理程序函数Debugger.Memory 继承了存储处理程序函数的访问器属性,供 SpiderMonkey 在被调试程序代码中发生给定事件时调用。

Debugger 的钩子不同,Debugger.Memory 的处理程序的返回值并不重要,会被忽略。处理程序函数接收 Debugger.Memory 所属的 Debugger 实例作为其 this 值。所属 DebuggeruncaughtExceptionHandler 仍然会为在 Debugger.Memory 钩子中抛出的错误触发。

在新的 Debugger.Memory 实例中,每个属性最初都是 undefined。分配给调试处理程序的任何值必须是函数或 undefined;否则会抛出一个 TypeError

处理程序函数在发生事件的同一线程中运行。它们在它们所属的隔间中运行,而不是在被调试程序的隔间中运行。

onGarbageCollection(statistics)

一个跨越一个或多个被调试程序的垃圾回收周期刚刚完成。

statistics 参数是一个包含有关 GC 周期信息的的对象。它具有以下属性

collections

The collections 属性的值是一个数组。由于 SpiderMonkey 的收集器是增量的,因此完整的收集周期可能包含多个离散的收集片段,JS mutator 交错运行。对于发生的每个收集片段,collections 数组中都有一个条目,其形式如下

{
  "startTimestamp": timestamp,
  "endTimestamp": timestamp,
}

这里的 timestamp 值是 GC 片段的开始和结束事件的 时间戳

reason

一个非常短的字符串,描述触发收集的原因。已知值包括以下内容

  • "API"

  • "EAGER_ALLOC_TRIGGER"

  • "DESTROY_RUNTIME"

  • "LAST_DITCH"

  • "TOO_MUCH_MALLOC"

  • "ALLOC_TRIGGER"

  • "DEBUG_GC"

  • "COMPARTMENT_REVIVED"

  • "RESET"

  • "OUT_OF_NURSERY"

  • "EVICT_NURSERY"

  • "FULL_STORE_BUFFER"

  • "SHARED_MEMORY_LIMIT"

  • "PERIODIC_FULL_GC"

  • "INCREMENTAL_TOO_SLOW"

  • "DOM_WINDOW_UTILS"

  • "COMPONENT_UTILS"

  • "MEM_PRESSURE"

  • "CC_FINISHED"

  • "CC_FORCED"

  • "LOAD_END"

  • "PAGE_HIDE"

  • "NSJSCONTEXT_DESTROY"

  • "SET_NEW_DOCUMENT"

  • "SET_DOC_SHELL"

  • "DOM_UTILS"

  • "DOM_IPC"

  • "DOM_WORKER"

  • "INTER_SLICE_GC"

  • "REFRESH_FRAME"

  • "FULL_GC_TIMER"

  • "SHUTDOWN_CC"

  • "USER_INACTIVE"

nonincrementalReason

如果 SpiderMonkey 的收集器确定无法增量收集垃圾,并且必须一次性执行完整 GC,则这是一个简短的字符串,描述它确定完整 GC 为必要的理由。否则,将返回 null。已知值包括以下内容

  • "GC mode"

  • "malloc bytes trigger"

  • "allocation trigger"

  • "requested"

gcCycleNumber

GC 周期的“编号”。不对应于已运行的 GC 周期的数量,但保证单调递增。

The Debugger.Memory.prototype 对象的函数属性

drainAllocationsLog()

trackingAllocationSitestrue 时,此方法返回调试目标集中最近的 Object 分配的数组。最近定义为自上次调用 drainAllocationsLog 以来,最近的 maxAllocationsLogLengthObject 分配。因此,调用此方法会有效地清除日志。

数组中的对象格式如下:

{
  "timestamp": timestamp,
  "frame": allocationSite,
  "class": className,
  "size": byteSize,
  "inNursery": inNursery,
}

其中

  • timestamp 是分配事件的时间戳

  • allocationSite 是分配站点(作为捕获的堆栈)。请注意,如果对象是在堆栈上没有 JavaScript 帧的情况下分配的,则此属性可以为 null。

  • className 是已分配对象的内部 [[Class]] 属性的字符串名称,例如“Array”、“Date”、“RegExp”或(最常见)“Object”。

  • byteSize 是对象的字节大小。

  • inNursery 如果分配发生在苗圃中,则为 true。如果分配跳过苗圃并在老年代堆中开始,则为 false。

trackingAllocationSitesfalse 时,drainAllocationsLog() 会抛出一个 Error

takeCensus(options)

对调试目标区间的內容进行普查。普查是对属于特定 Debugger 的调试目标的所有可达内存项的图进行完整遍历。普查会生成这些项的数量,并按各种标准进行细分。

options 参数是一个对象,其属性指定了如何进行普查。

如果 options 具有 breakdown 属性,则该属性确定普查如何对找到的项进行分类,以及收集有关这些项的哪些数据。例如,如果 dbg 是一个 Debugger 实例,则以下操作会对调试目标项进行简单计数

dbg.memory.takeCensus({ breakdown: { by: 'count' } })

这可能会产生如下结果:

{ "count": 1616, "bytes": 93240 }

以下是一个细分,它按 JavaScript 对象的类名对 JavaScript 对象进行分组,按 C++ 类型名称对非字符串、非脚本项进行分组,并按节点名称对 DOM 节点进行分组

{
  by: "coarseType",
  objects: { by: "objectClass" },
  other:   { by: "internalType" },
  domNode: { by: "descriptiveType" }
}

这会产生如下结果:

{
  "objects": {
    "Function":         { "count": 404, "bytes": 37328 },
    "Object":           { "count": 11,  "bytes": 1264 },
    "Debugger":         { "count": 1,   "bytes": 416 },
    "ScriptSource":     { "count": 1,   "bytes": 64 },
    // ... omitted for brevity...
  },
  "scripts":            { "count": 1,   "bytes": 0 },
  "strings":            { "count": 701, "bytes": 49080 },
  "other": {
    "js::Shape":        { "count": 450, "bytes": 0 },
    "js::BaseShape":    { "count": 21,  "bytes": 0 },
    "js::ObjectGroup":  { "count": 17,  "bytes": 0 }
  },
  "domNode": {
    "#text":            { "count": 1,   "bytes": 12 }
  }
}

通常,breakdown 值具有以下形式之一

  • { by: “count”, count:count, bytes:bytes }

    简单的分类:没有任何分类。只需统计访问的项的数量。如果 count 为 true,则统计访问的项的数量;如果 bytes 为 true,则统计项直接使用的字节数。countbytes 都是可选的;如果省略,则默认为 true。在普查的结果中,此细分会生成以下形式的值:

    { "count": n, "bytes": b }
    

    其中 countbytes 属性根据 breakdown 上的 countbytes 属性的存在情况而存在。

    请注意,普查只能为最常见的类型生成字节大小。当普查无法找到给定类型的字节大小时,它会返回零。

  • { by: “bucket” }

    不要进行任何过滤或分类。相反,为每个匹配的节点累积一个包含每个节点 ID 的桶。生成的报告是 ID 的数组。

    例如,要查找所有内部对象 [[class]] 属性名为“RegExp”的节点的 ID,可以使用以下代码

    const report = dbg.memory.takeCensus({
      breakdown: {
        by: "objectClass",
        then: { by: "bucket" }
      }
    });
    doStuffWithRegExpIDs(report.RegExp);
    
  • { by: “allocationStack”, then:breakdown, noStack:noStackBreakdown }

    按分配它们的完整 JavaScript 堆栈跟踪对项进行分组。

    使用 breakdown 进一步对每个不同堆栈上分配的所有项进行分类。

    在普查的结果中,此细分会生成一个 JavaScript Map 值,其键为 SavedFrame 值,其值为 breakdown 生成的任何类型的结果。在空 JavaScript 堆栈上分配的对象出现在键 null 下。

    只有在通过trackingAllocationSites标志请求时,SpiderMonkey 才会跟踪项的分配站点;即使这样,它也不会记录堆中出现的每种类型的项的分配站点。缺少分配站点信息的项使用 noStackBreakdown 进行计数。这些出现在结果 Map 中,键为字符串 "noStack"

  • { by: “objectClass”, then:breakdown, other:otherBreakdown }

    按其 ECMAScript [[Class]] 内部属性值对 JavaScript 对象进行分组。

    使用 breakdown 进一步对每个类中的 JavaScript 对象进行分类。使用 otherBreakdown 进一步对不是 JavaScript 对象的项进行分类。

    在普查的结果中,此细分会生成一个没有原型的 JavaScript 对象,其自己的属性名称是命名类的字符串,其值为 breakdown 生成的任何类型的结果。非对象项的结果显示为名为 "other" 的属性的值。

  • { by: “coarseType”, objects:objects, scripts:scripts, strings:strings, domNode:domNode, other:other }

    按其粗略类型对项进行分组。

    对作为 JavaScript 对象的项使用细分值 objects

    对作为 JavaScript 代码表示形式的项使用细分值 scripts。这包括字节码、编译后的机器代码和保存的源代码。

    对 JavaScript 字符串使用细分值 strings

    对 DOM 节点使用细分值 domNode

    对不属于上述任何类别的项使用细分值 other

    在普查的结果中,此细分会生成以下形式的 JavaScript 对象:

    {
      "objects": result,
      "scripts": result,
      "strings": result,
      "domNode:" result,
      "other": result,
    }
    

    其中每个 result 都是相应细分值生成的任何类型的值。所有细分值都是可选的,默认为 { type: "count" }

  • { by: "filename", then:breakdown, noFilename:noFilenameBreakdown }

    仅对脚本按脚本的文件名进行分组。

    使用 breakdown 进一步对每个不同文件名的所有脚本进行分类。

    缺少文件名的脚本使用 noFilenameBreakdown 进行计数。这些出现在结果 Map 中,键为字符串 "noFilename"

  • { by: "internalType", then: breakdown }

    按 SpiderMonkey 在内部为其类型指定的名称对项进行分组。这些名称对 Web 开发人员没有意义,但这种类型的细分确实充当了一种万能方法,对 Firefox 工具开发人员很有用。

    例如,按内部类型名称细分的原始调试目标全局变量的普查通常如下所示:

    {
      "JSString":        { "count": 701, "bytes": 49080 },
      "js::Shape":       { "count": 450, "bytes": 0 },
      "JSObject":        { "count": 426, "bytes": 44160 },
      "js::BaseShape":   { "count": 21,  "bytes": 0 },
      "js::ObjectGroup": { "count": 17,  "bytes": 0 },
      "JSScript":        { "count": 1,   "bytes": 0 }
    }
    

    在普查的结果中,此细分会生成一个没有原型的 JavaScript 对象,其自己的属性名称是命名类型的字符串,其值为 breakdown 生成的任何类型的结果。

  • [ breakdown, … ]

    使用所有给定的细分值对每个项进行分组。在普查的结果中,此细分会生成一个数组,其中包含每个列出的细分生成的值。

为了简化细分值,所有 thenother 属性都是可选的。如果省略,则将其视为 { type: "count" }

细分分组不能嵌套在自身内部。这将毫无用处,并且禁止此操作可以防止无限递归。

如果 options 参数没有 breakdown 属性,则 takeCensus 默认为以下内容:

{
  by: "coarseType",
  objects: { by: "objectClass" },
  domNode: { by: "descriptiveType" },
  other:   { by: "internalType" }
}

这会生成以下形式的结果:

{
  objects: { class: count, ... },
  scripts: count,
  strings: count,
  domNode: { node name:count, ... },
  other:   { type name:count, ... }
}

其中每个 count 具有以下形式:

{ "count": count, bytes: bytes }

由于执行普查需要遍历调试目标区间的对象整个图,因此这是一个代价高昂的操作。在 2014 年的开发人员硬件上,遍历包含大约 130,000 个节点和 410,000 条边的内存图大约需要 100 毫秒。遍历本身会暂时为每个节点分配一个哈希表条目(大约两个地址大小的字),此外还有每个类别的计数,其大小取决于类别的数量。

内存使用分析揭示了实现细节

内存分析可能会产生令人惊讶的结果,因为对内容 JavaScript 透明的浏览器实现细节通常会对内存消耗产生可见的影响。Web 开发人员需要了解其页面在真实浏览器上的实际内存消耗,因此工具公开这些行为是正确的,只要这样做能够帮助开发人员做出有关自身代码的决策。

本节介绍 Firefox 的实际行为偏离 Web 平台指定行为的一些方面。

对象

SpiderMonkey 对象通常使用的内存少于天真的“具有属性的属性表”模型建议的那样。例如,许多对象通常具有相同的属性集,只有属性的值在不同的对象之间有所不同。为了利用这种规律性,具有相同属性集的 SpiderMonkey 对象可以共享其属性元数据;只有属性值直接存储在对象中。

如果活动索引集密集,则数组对象也可以进行优化。

字符串

SpiderMonkey 有三种字符串表示形式

  • 普通:字符串的文本在其大小中计数。

  • 子字符串:字符串是其他某个字符串的子字符串,并指向该字符串以进行存储。这种表示形式可能会导致一个小字符串保留一个非常大的字符串。但是,字符串本身消耗的内存是一个很小的常数,与它的大小无关,因为它只是一个对更大字符串的引用、一个起始位置和一个长度。

  • 连接:当被要求连接两个字符串时,SpiderMonkey 可能会选择延迟复制字符串的数据,并将结果简单地表示为指向两个原始字符串的指针。同样,这样的字符串保留其他字符串,但字符串本身消耗的内存是一个很小的常数,与它的大小无关,因为它只是一对指针。

SpiderMonkey 会在它认为合适的时候将字符串从更复杂的表示形式转换为更简单的表示形式。此类转换通常会增加内存消耗。

SpiderMonkey 在所有网页和浏览器 JS 之间共享一些字符串。这些共享的字符串称为原子,不包含在普查的字符串计数中。

脚本

SpiderMonkey 具有 JavaScript 代码的复杂混合表示形式。内存中保留了四种表示形式

  • 源代码。SpiderMonkey 保留大多数 JavaScript 源代码的副本。

  • 压缩源代码。SpiderMonkey 会压缩 JavaScript 源代码,并在需要时解压缩它。启发式方法决定保留未压缩代码的时间长度。

  • 字节码。这是 SpiderMonkey 对 JavaScript 的解析表示。字节码可以直接解释,或用作即时编译器的输入。源代码按需解析为字节码;永远不会调用的函数永远不会被解析。

  • 机器代码。SpiderMonkey 包括几个即时编译器,每个编译器都将 JavaScript 源代码或字节码转换为机器代码。启发式方法决定编译哪些代码以及使用哪个编译器。机器代码可能会响应内存压力而被丢弃,并在需要时重新生成。

此外,SpiderMonkey 的即时编译器会为类型专门化生成内联缓存。此信息会定期丢弃以减少内存使用量。

在普查中,所有各种形式的 JavaScript 代码都放置在 "scripts" 类别中。