【问题标题】:MSBuild is failing inconsistently when performing a TFS build (usually error C1093 / Not enough Storage)执行 TFS 构建时 MSBuild 失败不一致(通常是错误 C1093/存储空间不足)
【发布时间】:2015-02-16 11:32:45
【问题描述】:

我有一个非常奇怪且难以诊断的 MSBuild/TFS 问题。我有一个包含大约 12 种不同构建配置的解决方案。在构建服务器上运行时,构建批次可能需要 30 分钟,并且已经运行了数周,但现在偶尔会失败。

大多数时候,当它失败时会出现这样的错误:

19:25:45.037 2>TestPlanDocument.cpp(1):致命错误 C1093:API 调用“GetAssemblyRefHash”失败“0x8007000e”:错误消息:没有足够的存储空间来完成此操作。 [C:\Builds\1\ICCSim Card Test Controller\ICCSimCTC Release\src\CardTestController\CardTestController.vcxproj]

错误有时会发生在不同的文件上。它也不会发生在每个构建配置上,它非常不一致,有时甚至会成功构建所有配置。构建配置之间也没有太大差异,主要只是一些字符串更改,当然它们都可以在本地构建。

有问题的 API 调用通常 GetAssemblyRefHash 但并非总是如此。我不认为这是问题所在,因为在谷歌上搜索 GetAssemblyRefHash 几乎没有发现任何问题。我怀疑这里存在某种资源问题,但我不知道是什么:有足够的 HDD 空间(数百 GB),大量的 RAM(机器最初分配了 4GB 的最小值,但它是动态的,因为它是 Hyper -v - 它从未超过 2.5GB。为了以防万一,我将它提升到最低 8GB,但没有任何变化。

我已将构建详细程度设置为诊断,它并没有真正显示任何其他有用的信息,只是同样的错误。

作为参考,构建服务器在所有补丁上都是最新的。它运行的是 Windows Server 2012 R2,安装了 TFS 2013 和 VS 2013,两者都在更新 4 上。

此时我真的很茫然,如果有任何帮助或指点,我将不胜感激。

编辑:为了让人们了解最新情况,编译工具链处于 32 位模式,但即使切换到 64 位,问题仍然存在。

【问题讨论】:

  • 您使用的是64-bit toolset吗? /MP 选项怎么样?
  • 嗨 Colin - 这是一个正在构建的 32 位应用程序,但它使用的是 64 位版本的 MSBuild。我没有尝试修改 /MP 选项,目前我相信它正在使用默认值,所以也许值得玩弄。
  • 我建议的解决方案修复了一些问题,这些问题在构建过程中遇到内存和文件锁定问题时会出现各种错误代码,并且可以消除尖峰资源使用。防病毒程序可能会导致类似的问题。您已经尝试了我的下一个建议,即增加 VM 中的最小 RAM。不过,我不确定这是问题所在。您的项目是否有大量的嵌入式资源?
  • 有时构建服务器上的空间问题实际上并不是空间问题,您可以进入 %temp% 文件夹(用于构建代理帐户)并验证该文件夹中实际有多少文件。如果有超过 64K 的文件(不是空间,数量),就会发生一些奇怪的事情。不确定这是否有帮助,但无论如何删除文件都不会丢失。
  • @Colin Robertson:仍然对这个问题不满意。我一直在监视 GDI 对象、句柄、线程等的性能,似乎没有任何东西接近极限。我已经增加了 RAM,我已经增加了页面文件,甚至尝试将它移动到另一个磁盘但没有。该项目也没有任何大量的嵌入式资源。它有一些文本文件,每个文件可能 500 字节,仅此而已。

标签: tfs visual-studio-2013 msbuild


【解决方案1】:

好的,我有一个更新!我向 Microsoft 开了一张支持票,并一直忙于与他们合作以找出问题所在。 他们走上了上述相同的道路并得出了相同的结论 - 这不是资源问题

长话短说,微软现在承认这可能是 VC++ 编译器中的一个错误,这几乎可以肯定是由竞争条件引起的(尽管这尚未得到证实)。没有关于他们是否会在未来的版本中修复它的消息。

有一种解决方法,即在项目级别使用 /MP 标志来限制 MSBuild 打开的编译器进程的数量,而无需完全禁用多个实例(这对我来说是构建时间加倍)。

为此,请转到您的项目属性并在配置属性 -> C/C++ -> 命令行下,您需要指定 /MP 标志,然后指定一个数字来限制进程的数量。

我的构建服务器有 8 个虚拟 CPU,正常行为相当于 /MP8,但这会导致错误有时出现。对我来说,使用 /MP4 似乎足以限制错误,而不会导致构建时间增加太多。如果您发现与此类似的错误,您可能需要尝试使用其他数字,例如 /MP6 或 /MP2。

【讨论】:

  • 为了向这个问题添加更多信息(是的,编译器团队仍在继续),我们已将问题缩小为 DFS 文件共享问题。如果程序集位于文件共享上,这绝对没问题,但是如果文件共享通过 DFS 共享(类似于 \\\share\assemblies),则会出现问题。
【解决方案2】:

我想我找到了来源,但我仍然不知道原因。

浏览Microsoft Shared Source,可以找到GetAssemblyRefHash()的出处:

HRESULT CAsmLink::GetAssemblyRefHash(mdToken FileToken, const void** ppvHash, DWORD* pcbHash)
{
    if (TypeFromToken(FileToken) != mdtAssemblyRef) {
        VSFAIL( "You can only get AssemblyRef hashes for assemblies!");
        return E_INVALIDARG;
    }

    HRESULT hr;
    CAssembly *file = NULL;
    if (FAILED(hr = m_pImports->GetFile( FileToken, (CFile**)&file)))
        return hr;

    return file->GetHash(ppvHash, pcbHash);
}

这里只有两个地方需要调查——调用m_pImports->GetFile(),其中m_pImportsCAssembly *m_pImports;,另一个是file->GetHash()

m_pImports->GetFile() 在这里,而且是死胡同:

HRESULT CAssembly::GetFile(DWORD index, CFile** file)
{
    if (!file)
        return E_POINTER;

    if (RidFromToken(index) < m_Files.Count()) {
        if ((*file = m_Files.GetAt(RidFromToken(index))))
            return S_OK;
    }
    return ReportError(E_INVALIDARG);
}

file-&gt;GetHash(),这里是:

HRESULT CAssembly::GetHash(const void ** ppvHash, DWORD *pcbHash)
{
    ASSERT( ppvHash && pcbHash);
    if (IsInMemory()) {
        // We can't hash an InMemory file
        *ppvHash = NULL;
        *pcbHash = 0;
        return S_FALSE;
    }

    if (!m_bDoHash || (m_cbHash && m_pbHash != NULL)) {
        *ppvHash = m_pbHash;
        *pcbHash = m_cbHash;
        return S_OK;
    }

    DWORD cchSize = 0, result;

    // AssemblyRefs ALWAYS use CALG_SHA1
    ALG_ID alg = CALG_SHA1;
    if (StrongNameHashSize( alg, &cchSize) == FALSE)
        return ReportError(StrongNameErrorInfo());

    if ((m_pbHash = new BYTE[cchSize]) == NULL)
        return ReportError(E_OUTOFMEMORY);
    m_cbHash = cchSize;

    if ((result = GetHashFromAssemblyFileW(m_Path, &alg, (BYTE*)m_pbHash, cchSize, &m_cbHash)) != 0) {
        delete [] m_pbHash;
        m_pbHash = 0;
        m_cbHash = 0;
    }
    *ppvHash = m_pbHash;
    *pcbHash = m_cbHash;

    return result == 0 ? S_OK : ReportError(HRESULT_FROM_WIN32(result));
}

我们可以看到,大约一半的时候,它尝试分配空间来存储 byte[] 结果,失败时返回 E_OUTOFMEMORY,也就是你看到的错误代码:

if ((m_pbHash = new BYTE[cchSize]) == NULL)
    return ReportError(E_OUTOFMEMORY);
m_cbHash = cchSize;

还有其他途径需要考虑,但这似乎是最明显的来源。所以看起来问题在于普通的内存分配失败了。

这是什么原因造成的?

  • 缺少可用物理内存页/交换
  • 进程内存碎片。
  • 无法在交换文件中为此保留提交空间
  • 地址空间不足

在这一点上,我最好的猜测是内存碎片。您是否三次检查过 Microsoft CPP 编译器是否在 64 位模式下运行?或许看看您是否可以调试 编译器(Microsoft 符号服务器可能会在此处为您提供帮助),并为该行设置断点并在发生时转储堆。

关于诊断堆碎片的一些细节 - 当编译器崩溃时启动 sysinternal 的 VMMap,并查看空闲列表 - 你需要 three chunks at least 64 kB free to perform an allocation;小于 64 kB 且不会被使用,并保留两个 64 kB 块。

【讨论】:

  • 好吧,我以为我在做某事。我仔细检查了一下,虽然 MSBuild 作为 64 位进程运行,但使用的 C++ 编译器仍然是 32 位版本 - 嘘。我做了一些挖掘并在 MSbuild 中找到了正确的参数来使用 64 位编译器链并启动构建。我使用进程资源管理器来确认各种进程正在运行 64 位,而且它们确实如此。 ...并且错误仍在发生!一个 64 位进程到底怎么会在分配内存时遇到问题?备案,机器本身还有oodles and oodles of memory free
  • 好的,我正在尝试进一步调试,但我正在努力确定我需要调试的进程。问题是,使用 /MP 时会出现问题,这意味着多个 CL.exe 进程会很快出现和消失 - 您将如何调试它?不可能提前知道哪个过程会失败。
猜你喜欢
  • 2020-11-15
  • 1970-01-01
  • 1970-01-01
  • 2020-06-18
  • 1970-01-01
  • 1970-01-01
  • 2019-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多