Sentry 开发者贡献指南 - SDK 开发(性能监控:Sentry SDK API 演进)
内容整理自官方开发文档
本文档的目标是将 Sentry SDK
中性能监控
功能的演变置于上下文中。
我们首先总结了如何将性能监控
添加到 Sentry
和 SDK
,
然后我们讨论 identified issues(已确定的问题)
吸取的经验教训以及解决这些问题的举措。
介绍
早在 2019
年初,Sentry
就开始尝试向 SDK
添加跟踪功能。
Python 和 JavaScript SDK 是设计和开发第一个概念的测试平台。
概念验证于 2019 年 4 月 29 日 发布,
并于 2019 年 5 月 7 日交付给 Sentry。
Python
和 JavaScript
是显而易见的选择,因为它们允许我们试验检测 Sentry
自己的后端
和前端
。
- https://github.com/getsentry/sentry-python/pull/342
- https://github.com/getsentry/sentry-javascript/pull/1918
- https://github.com/getsentry/sentry-python/releases/tag/0.7.13
- https://github.com/getsentry/sentry/pull/12952
请注意,上述工作与 OpenCensus 和 OpenTracing 合并形成 OpenTelemetry 是同时代的。
Sentry
的 API
和 SDK
实现借鉴了 OpenTelemetry 1.0
之前版本的灵感,并结合了我们自己的想法。
例如,我们的 Span 状态列表与 2019 年底左右在 OpenTelemetry 规范中可以找到的匹配。
- https://medium.com/opentracing/a-roadmap-to-convergence-b074e5815289
- https://github.com/getsentry/relay/blob/55127c75d4eeebf787848a05a12150ee5c59acd9/relay-common/src/constants.rs#L179-L181
使用 API
后,性能监控支持随后扩展到其他 SDK
。Sentry 的性能监控 解决方案于 2020 年 7 月普遍可用。
OpenTelemetry 的跟踪规范 1.0 版于 2021 年 2 月发布。
- https://blog.sentry.io/2020/07/14/see-slow-faster-with-performance-monitoring
- https://medium.com/opentelemetry/opentelemetry-specification-v1-0-0-tracing-edition-72dd08936978
我们最初的实现重用了我们现有的错误报告机制:
- Event type 扩展了新字段。 这意味着我们可以节省时间并快速开始向
Sentry
发送事件
,而不是设计
和实现
全新的摄取管道,这一次,不是error
,而是一种新的transaction
事件类型。 - 由于我们只是发送一种新型事件,因此也重用了
SDK
传输层。 - 由于我们共享
摄取管道(ingestion pipeline)
,这意味着我们共享存储以及发生在所有事件上的处理的许多部分。
我们的实现演变成明确强调 Transaction
和 Span
之间的区别。部分原因是重用 Event 接口的副作用。
Transaction
与客户产生了良好的共鸣。
他们允许突出显示代码中的重要工作块,例如浏览器页面加载或 http 服务器请求。
客户可以查看和浏览 transaction
列表,而在 transaction
中,span
为更细粒度的工作单元提供详细的时间安排。
在下一节中,我们将讨论当前模型的一些缺点。
已确定的问题
虽然统一 SDK 架构(hub
、client
、scope
)
和 transaction ingestion
模型的重用有其优点,但经验揭示了一些我们将其分为两类的问题。
第一组与 scope
传播有关,本质上是确定 当前 scope
是什么的能力。用户代码中的手动检测
以及 SDK 集成
中的自动检测
都需要此操作。
第二组是与用于将 transaction
数据从 SDK
发送到 Sentry
的 wire
格式相关的问题。
Scope 传播
该问题由 getsentry/sentry-javascript#3751 跟踪。
Unified SDK 架构 基本上是基于每个并发单元存在一个 hub
,每个 hub
有一堆 client
和 scope
对。
Client
保存配置并负责通过 transport
向 Sentry 发送数据,而 scope
保存附加到传出事件(例如 tag
和 breadcrumb
)的上下文数据。
每个 hub
都知道当前的 scope
是什么。它始终是堆栈顶部的 scope
。困难的部分是 “per unit of concurrency(每单位并发)”
有一个 hub
。
例如,JavaScript
是具有事件循环和异步代码执行的单线程。
没有标准的方法来承载跨异步调用工作的上下文数据。
因此,对于 JavaScript
浏览器应用程序,只有一个全局 hub
共享用于同步
和异步
代码。
类似的情况出现在 Mobile SDK
上。
用户期望上下文数据(例如 tags
、current user
是什么、
breadcrumbs
以及存储在 scope
上的其他信息)可以从任何线程获得和设置。
因此,在这些 SDK
中,只有一个全局 hub
。
在这两种情况下,当 SDK
必须处理 reporting errors
时,一切都相对较好。
随着跟踪 transaction
和 span
的额外责任,scope
变得不适合存储当前的 span
,因为它限制了并发 span
的存在。
对于浏览器 JavaScript
,一个可能的解决方案是使用 Zone.js,Angular
框架的一部分。
主要挑战是它增加了包的大小,并且可能会无意中影响最终用户应用程序,因为它对 JavaScript
运行时引擎的关键部分进行了猴子修补(monkey-patches)
。
当我们尝试为手动检测创建更简单的 API
时,scope
传播问题变得尤为明显。
这个想法是公开一个 Sentry.trace
函数,该函数将隐式传播 tracing
和 scope
数据,
并支持同步和异步代码的深度嵌套。
举个例子,假设有人想测量搜索 DOM
树需要多长时间。Tracing(跟踪)
此操作将如下所示:
await Sentry.trace(
{
op: 'dom',
description: 'Walk DOM Tree',
},
async () => await walkDomTree()
);
使用 Sentry.trace
功能,用户在添加计时数据时不必担心保留对正确 transaction
或 span
的引用。
用户可以在 walkDomTree
函数中自由创建子 Span
,Span
将在正确的层次结构中排序。
实际 trace
函数的实现相对简单
(参见具有示例实现的 PR)。
然而,了解异步代码和全局集成中的当前 span
是一个尚未克服的挑战。
以下两个示例综合了 scope
传播问题。
无法确定当前 Span
考虑一些需要获取对当前 span
的引用的自动检测代码,在这种情况下,手动 scope
传播不可用。
// SDK code
function fetchWrapper(/* ... */) {
/*
... some code omitted for simplicity ...
*/
const parent = getCurrentHub().getScope().getSpan(); // <1>
const span = parent.startChild({
data: { type: 'fetch' },
description: `${method} ${url}`,
op: 'http.client',
});
try {
// ...
// return fetch(...);
} finally {
span.finish();
}
}
window.fetch = fetchWrapper;
// User code
async function f1() {
const hub = getCurrentHub();
let t = hub.startTransaction({ name: 't1' });
hub.getScope().setSpan(t);
try {
await fetch('https://example.com/f1');
} finally {
t.finish();
}
}
async function f2() {
const hub = getCurrentHub();
let t = hub.startTransaction({ name: 't2' });
hub.getScope().setSpan(t);
try {
await fetch('https://example.com/f2');
} finally {
t.finish();
}
}
Promise.all([f1(), f2()]); // run f1 and f2 concurrently
在上面的例子中,几个并发的 fetch
请求触发了 fetchWrapper
helper 的执行。 行 <1>
必须能够根据当前的执行流程观察到不同的 span
,导致如下两个 span
树:
t1
\
|- http.client GET https://example.com/f1
t2
\
|- http.client GET https://example.com/f2
这意味着,当 f1
运行时,parent
必须引用 t1
,而当 f2
运行时,parent
必须是 t2
。
不幸的是,上面的所有代码都在争先恐后地更新和读取单个 hub
实例,因此观察到的 span
树不是确定性的。例如,结果可能错误地为:
t1
t2
\
|- http.client GET https://example.com/f1
|- http.client GET https://example.com/f2
作为无法正确确定当前 span
的副作用,
fetch
集成的显示实现(和其他)在JavaScript 浏览器 SDK 中选择创建 flat transactions,
其中所有子 span
都是 transaction
的直接子代(而不是具有适当的多级树结构)。
请注意,其他跟踪库也面临同样的挑战。
在 OpenTelemetry for JavaScript
中有几个(在开放时)问题与确定父跨度和正确的上下文传播(包括异步代码)相关:
- 如果使用多个 TracerProvider 实例,则上下文泄漏 #1932
- 如何在不传递 parent 的情况下创建嵌套 span #1963
- 嵌套的子 span 没有得到正确的父级 #1940
- OpenTracing shim 不会改变上下文 #2016
- Http Span 未链接/未设置父 Span #2333
相互冲突的数据传播预期
每当我们添加前面讨论过的 trace
函数,或者只是尝试使用 Zones
解决 scope
传播时,就会出现预期冲突。
当前的 span
与 tags
、breadcrumbs
等一起存储在 scope
中的事实使数据传播变得混乱,
因为 scope
的某些部分旨在仅传播到内部函数调用中(例如,tags
),
而其他人预计会传播回调用者(例如,breadcrumbs
),尤其是在出现 error
时。
这是一个例子:
function a() {
trace((span, scope) => {
scope.setTag('func', 'a');
scope.setTag('id', '123');
scope.addBreadcrumb('was in a');
try {
b();
} catch(e) {
// How to report the SpanID from the span in b?
} finally {
captureMessage('hello from a');
// tags: {func: 'a', id: '123'}
// breadcrumbs: ['was in a', 'was in b']
}
})
}
function b() {
trace((span, scope) => {
const fail = Math.random() > 0.5;
scope.setTag('func', 'b');
scope.setTag('fail', fail.toString());
scope.addBreadcrumb('was in b');
captureMessage('hello from b');
// tags: {func: 'b', id: '123', fail: ?}
// breadcrumbs: ['was in a', 'was in b']
if (fail) {
throw Error('b failed');
}
});
}
在上面的示例中,如果 error
在调用堆栈中冒泡,我们希望能够报告 error
发生在哪个 span
(通过引用 SpanID
)。
我们希望有面包屑来描述发生的一切,无论哪个 Zones
正在执行,
我们希望在内部 Zone
中设置一个 tag
来覆盖来自父 Zone
的同名 tag
,
同时继承来自父 Zone
的所有其他 tag
。每个 Zone
都有自己的 "current span"
。
所有这些不同的期望使得很难以一种可以理解的方式重用当前的 scope
概念、面包屑的记录方式以及这些不同的概念如何相互作用。
最后,值得注意的是,在不破坏现有 SDK API
的情况下,重组 scope
管理的更改很可能无法完成。
现有的 SDK
概念 — 如 hubs
、scopes
、breadcrumbs
、user
、tags
和 contexts
— 都必须重新建模。
Span 摄取模型
考虑由以下 span
树描述的跟踪:
F*
├─ B*
│ ├─ B
│ ├─ B
│ ├─ B
│ │ ├─ S*
│ │ ├─ S*
│ ├─ B
│ ├─ B
│ │ ├─ S*
│ ├─ B
│ ├─ B
│ ├─ B
│ │ ├─ S*
where
F: span created on frontend service
B: span created on backend service
S: span created on storage service
此跟踪说明了 3
个被检测的服务,当用户单击网页上的按钮 (F
) 时,后端 (B
) 执行一些工作,然后需要对存储服务 (S
) 进行多次查询。位于给定服务入口点的 Span
标有 *
以表示它们是 transaction
。
我们可以通过这个例子来比较和理解 Sentry
的 span
摄取模型与 OpenTelemetry
和其他类似跟踪系统使用的模型之间的区别。
在 Sentry
的 span
摄取模型中,属于 transaction
的所有 span
必须在单个请求中一起发送。
这意味着在整个 B*
transaction 期间,所有 B
span 都必须保存在内存中,包括在下游服务(示例中的存储服务)上花费的时间。
在 OpenTelemetry
的模型中,span
在完成时被一起批处理,并且一旦 a)
批次中有一定数量的 span
或 b)
过了一定的时间就会发送批次。
在我们的示例中,这可能意味着前 3
个 B
跨度将一起批处理并发送,
而第一个 S*
事务仍在存储服务中进行。随后,其他 B
span 将一起批处理并在完成时发送,直到最终 B*
transaction span 也被发送。
虽然 transaction
作为将 span
组合在一起并探索 Sentry
中感兴趣的操作的一种方式特别有用,
但它们目前存在的形式会带来额外的认知负担。
SDK
维护人员和最终用户在编写检测代码时都必须了解并在 transaction
或 span
之间进行选择。
在当前的摄取模型中已经确定了接下来几节中的问题,并且都与这种二分法有关。
事务的复杂 JSON 序列化
在 OpenTelemetry
的模型中,
所有跨度都遵循相同的逻辑格式。
用户和检测库可以通过将 key-value
属性附加到任何 span
来为其提供更多含义。
wire
协议使用 span
列表将数据从一个系统发送到另一个系统。
与 OpenTelemetry
不同,Sentry
的模型对两种类型的 span
进行了严格区分:transaction span
(通常称为 transactions
)和 regular span
。
在内存中,transaction span
和 regular span
有一个区别:transaction span
有一个额外的属性,即 transaction name
。
但是,当序列化为 JSON
时,差异更大。
Sentry SDK
以直接类似于内存中的 span
的格式将常规 span
序列化为 JSON
。
相比之下,transaction span
的序列化需要将其 span
属性映射到 Sentry Event
(最初用于 report errors
,扩展为专门用于 transactions
的新字段),并将所有子 span
作为列表嵌入 Event
中。
Transaction Span 获取 Event 属性
当 transaction
从其内存表示转换为 Event
时,
它会获得更多无法分配给 regular span
的属性,
例如 breadcrumbs
, extra
, contexts
, event_id
, fingerprint
, release
, environment
, user
等。
生命周期钩子
Sentry SDK
为 error
事件公开了一个 BeforeSend
hook,允许用户在将事件
发送到 Sentry
之前修改
和/或丢弃
事件。
当引入新的 transaction
类型事件时,很快就决定此类事件不会通过 BeforeSend
hook,主要有两个原因:
- 防止用户代码依赖
transaction
的双重形式(有时看起来像一个span
,有时像一个event
,如前几节所述); - 为了防止现有的
BeforeSend
函数在编写时只考虑到error
而干扰transaction
,无论是意外地改变它们、完全丢弃它们,还是导致一些其他意想不到的副作用。
然而,也很明显需要某种形式的 lifecycle hook
,以允许用户执行诸如更新 transaction
名称之类的操作。
我们最终达成了中间立场,即通过使用 EventProcessor
(一种更通用的 BeforeSend
形式)来允许更改/丢弃
transaction 事件。
这通过在数据离开 SDK
之前让用户立即访问他们的数据来解决问题,但它也有缺点,它比 BeforeSend
使用起来更复杂,并且还暴露了从未打算泄漏的 transaction
二元性。
相比之下,在 OpenTelemetry
中,span
通过 span processor
,这是两个生命周期钩子:一个是在 span
开始时,一个是在它结束时。
嵌套事务
Sentry
的摄取模型不是为服务中的嵌套 transaction
而设计的。Transaction
旨在标记服务转换。
在实践中,SDK
无法防止 transaction
嵌套。最终结果可能会让用户感到惊讶,因为每笔 transaction
都会开始一棵新树。关联这些树的唯一方法是通过 trace_id
。
Sentry 的计费模型是针对每个事件的,无论是 error
事件还是 transaction
事件。这意味着 transaction
中的 transaction
会生成两个可计费事件。
在 SDK
中,在 transaction
中进行 transaction
将导致内部 span
被围绕它们的最内层 transaction
“吞噬”。
在这些情况下,创建 span
的代码只会将它们添加到两个 transaction
之一,从而导致另一个 transaction
中的检测间隙。
Sentry
的 UI
并非旨在以有用的方式处理嵌套 transaction
。
当查看任何一个 transaction
时,就好像 transaction
中的所有其他 transaction
都不存在(树视图上没有直接表示其他 transaction
)。
有一个 trace view
功能来可视化共享一个 trace_id
的所有 transaction
,
但 trace view
仅通过显示 transaction
而不是子 span
来提供跟踪的概述。如果不先访问某个 transaction
,就无法导航到 trace view
。
对于这种情况(伪代码),用户对 UI
中的期望也存在混淆:
# if do_a_database_query returns 10 results, is the user
# - seeing 11 transactions in the UI?
# - billed for 11 transactions?
# - see spans within create_thumbnail in the innermost transaction only?
with transaction("index-page"):
results = do_a_database_query()
for result in results:
if result["needs_thumbnail"]:
with transaction("create-thumbnail", {"resource": result["id"]}):
create_thumbnail(result)
跨度不能存在于事务之外
Sentry
的追踪体验完全围绕着存在于 transaction
中的 trace
部分。这意味着数据不能存在于 transaction
之外,即使它存在于 trace
中。
如果 SDK
没有进行 transaction
,则由 instrumentation
创建的 regular span
将完全丢失。
也就是说,这对 Web server
来说不是什么问题,因为自动检测的 transaction
随着每个传入请求开始和结束。
Transaction
的要求在前端(浏览器、移动和桌面应用程序)上尤其具有挑战性,
因为在这些情况下,自动检测的 transaction
不太可靠地捕获所有 span
,因为它们在自动完成之前只持续有限的时间。
在 trace
以仅作为 span
而不是 transaction
进行检测的操作开始的情况下,会出现另一个问题。在我们的 示例跟踪
中,产生 trace
的第一个 span
是由于单击按钮。
如果按钮点击 F*
被检测为常规的 span
而不是 transaction
,则很可能不会捕获来自前端的数据。然而,仍会捕获 B
和 S
span,导致不完整的踪迹。
在 Sentry
的模型中,如果一个 span
不是一个 transaction
并且没有作为 transaction
的祖先 span
,那么该 span
将不会被摄取。
反过来,这意味着在很多情况下,跟踪丢失了有助于调试问题的关键信息,特别是在前端,transaction
需要在某个时刻结束但执行可能会继续。
自动和手动检测面临着决定是开始 span
还是 transaction
的挑战,考虑到以下因素,决定尤其困难:
- 如果没有
transaction
,则span
丢失。 - 如果已经存在
transaction
,则存在嵌套事务
问题。
缺少 Web Vitals 测量
Sentry
的浏览器工具收集 Web Vitals
测量值。但是,因为这些测量值是使用自动检测的 transaction
作为载体发送到 Sentry
的,所以在自动 transaction
完成后由浏览器提供的测量值将丢失。
这会导致 transaction
丢失一些 Web Vitals
或对 LCP
等指标进行非最终测量。
前端事务持续时间不可靠
因为所有的数据都必须在一个 transaction
中。Sentry
的浏览器 SDK
为每个页面加载和每个导航创建一个 transaction
。这些 transaction
必须在某个时间结束。
如果在 transaction
完成之前关闭浏览器选项卡并将其发送到 Sentry
,则所有收集的数据都会丢失。
因此,SDK 需要平衡丢失所有数据的风险与收集不完整和可能不准确的数据的风险。
在观察到最后一个活动(例如传出的 HTTP
请求)后空闲了一段时间后,Transaction
就完成了。
这意味着页面加载
或导航 transaction
的持续时间是一个相当随意的值,不一定能改进或与其他事务相比,因为它不能准确代表任何具体和可理解的过程的持续时间。
我们通过将 LCP Web Vital
作为浏览器的默认性能指标来应对这一限制。
但是,如上所述,LCP
值可能会在最终确定之前发送,因此这不是理想的解决方案。
内存缓冲影响服务器
如前所述,当前的摄取模型需要 Sentry SDK
来观察内存中的完整 span
树。
以恒定的并发 transaction
流运行的应用程序将需要大量的系统资源来收集和处理跟踪数据。Web 服务器是出现此问题的典型案例。
这意味着记录 100%
的 span
和 100%
的 transaction
对于许多服务器端应用程序来说是不可行的,因为所产生的开销太高了。
无法批处理事务
Sentry
的摄取模型不支持一次摄取多个事件。特别是,SDK
不能将多个 transaction
批处理为一个请求。
因此,当多笔 transaction
几乎同时完成时,SDK
需要为每个 transaction
发出单独的请求。
这种行为在最好的情况下是非常低效的,在最坏的情况下是对资源(如网络带宽和CPU周期)的严重且有问题的消耗。
兼容性
Transaction Span
的特殊处理与 OpenTelemetry
不兼容。使用 OpenTelemetry SDK
检测现有应用程序的用户无法轻松使用 Sentry
来获取和分析他们的数据。
Sentry
确实为 OpenTelemetry Collector
提供了一个 Sentry Exporter
,但是,由于当前的摄取模型,Sentry Exporter 有一个主要的正确性限制。
总结
通过在 Sentry
中构建当前的跟踪实现,我们学到了很多。
本文档试图捕捉许多已知的限制,以作为未来改进的基础。
追踪是一个复杂的主题,驯服这种复杂性并非易事。
第一组中的问题 - 与 scope propagation(作用域传播) 相关的问题 - 是 SDK
及其设计方式独有的问题。
解决这些问题将需要对所有 SDK
进行内部架构更改,包括重新设计面包屑
等旧功能,
但进行此类更改是实现简单易用的 tracing helper
(如可在任何上下文中工作并捕获准确可靠的性能数据的 trace
函数)的先决条件。
请注意,此类更改几乎肯定意味着发布新的主要 SDK
版本,这会破坏与现有版本的兼容性。
第二组中的问题 - 与 span ingestion model(跨度摄取模型) 相关的问题要复杂得多,因为为解决这些问题所做的任何更改都会影响产品的更多部分,并且需要多个团队的协调努力。
尽管如此,对 ingestion model
进行更改将对产品产生不可估量的积极影响,因为这样做会提高效率,使我们能够收集更多数据,并减少 instrumentation
的负担。
系列
Sentry 开发者贡献指南 - SDK 开发(性能监控:Sentry SDK API 演进)的更多相关文章
- Sentry 开发者贡献指南 - 数据库迁移
Django 迁移是我们处理 Sentry 中数据库更改的方式. Django 迁移官方文档:https://docs.djangoproject.com/en/2.2/topics/migratio ...
- Sentry 开发者贡献指南 - 配置 PyCharm
概述 如果您使用 PyCharm 进行开发,则需要配置一些内容才能运行和调试. 本文档描述了一些对 sentry 开发有用的配置 配置 Python 解释器:(确保它是 venv 解释器)例如 ~/v ...
- Sentry 开发者贡献指南 - SDK 开发(性能监控)
内容整理于官方开发文档 系列 Docker Compose 部署与故障排除详解 K8S + Helm 一键微服务部署 Sentry 开发者贡献指南 - 前端(ReactJS生态) Sentry 开发者 ...
- Sentry 开发者贡献指南 - SDK 开发(事件负载)
内容整理自官方开发文档 系列 Docker Compose 部署与故障排除详解 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本 快速使用 Docker 上手 Sentr ...
- Sentry 开发者贡献指南 - 后端服务(Python/Go/Rust/NodeJS)
内容整理自官方开发文档 系列 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本 快速使用 Docker 上手 Sentry-CLI - 30 秒上手 Source Map ...
- Sentry 开发者贡献指南 - Feature Flag
功能 flag 在 Sentry 的代码库中声明. 对于自托管用户,这些标志然后通过 sentry.conf.py 进行配置. 对于 Sentry 的 SaaS 部署,Flagr 用于在生产中配置标志 ...
- Sentry 开发者贡献指南 - Django Rest Framework(Serializers)
Serializer 用于获取复杂的 python 模型并将它们转换为 json.序列化程序还可用于在验证传入数据后将 json 反序列化回 Python 模型. 在 Sentry,我们有两种不同类型 ...
- Sentry 开发者贡献指南 - 前端 React Hooks 与虫洞状态管理模式
系列 Sentry 开发者贡献指南 - 前端(ReactJS生态) Sentry 开发者贡献指南 - 后端服务(Python/Go/Rust/NodeJS) 什么是虫洞状态管理模式? 您可以逃脱的最小 ...
- Sentry 开发者贡献指南 - 前端(ReactJS生态)
内容整理自官方开发文档 系列 1 分钟快速使用 Docker 上手最新版 Sentry-CLI - 创建版本 快速使用 Docker 上手 Sentry-CLI - 30 秒上手 Source Map ...
随机推荐
- 【STM32】使用SDIO进行SD卡读写,包含文件管理FatFs(终)-配合内存管理来遍历SD卡
[STM32]使用SDIO进行SD卡读写,包含文件管理FatFs(一)-初步认识SD卡 [STM32]使用SDIO进行SD卡读写,包含文件管理FatFs(二)-了解SD总线,命令的相关介绍 [STM3 ...
- APK 反编译以及遇到的问题
APK反编译: https://www.cnblogs.com/geeksongs/p/10864200.html 遇到的问题 https://www.jianshu.com/p/55bf5f688e ...
- VScode 使用 CMake 入门
参考 CMake 入门实战 在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下: 编写 CMake 配置文件 CMakeLists.txt . 执行命令 cmake PA ...
- centos7.4 64位安装 git
参考博客:Linux Jenkins配置Git 1. git --version 查看有没有安装 过 git,没有则 继续 2. git 压缩包下载地址:https://mirrors.edge.ke ...
- Go modules基础精进,六大核心概念全解析(上)
点击一键订阅<云荐大咖>专栏,获取官方推荐精品内容,学技术不迷路! Go 语言做开发时,路径是如何定义的?Go Mudules又为此带来了哪些改变?本文将会全面介绍Go modules六大 ...
- intelliJ破解及JavaEE搭建
intellij2020.3破解 转载自https://www.exception.site/essay/how-to-free-use-intellij-idea-2019-3 第一步: 下载最新的 ...
- Android: Client-Server communication by JSON
Refer to: http://osamashabrez.com/client-server-communication-android-json/ This is a sequel to my l ...
- 人工水母搜索算法—matlab代码
clc clear foj = @ Sphere; Lb = -100; % 搜索空间下界 Ub = 100; % 搜索空间上界 N_iter = 1000; % 最大迭代次数 n_pop = 50; ...
- CF1557B Moamen and k-subarrays 题解
Content 给定一个大小为 \(n\) 的数组.你可以将其分为 \(k\) 个子数组,并按照每个子数组的字典序重新排列这些子数组,再顺次拼接,得到一个新的数组.问是否存在一种划分子数组的方案,使得 ...
- Vim使用简介
Vim操作 Vim真的很酷:D 编辑模式 正常模式:在文件中四处移动光标进行修改 插入模式:插入文本 替换模式:替换文本 可视化(一般,行,块)模式:选中文本块 命令模式:用于执行命令 在不同的操作模 ...