hexo seo 优化

阅读之前#

你需要知道的知识包括#

  • Hexo 基本命令
  • html<meta>标签

对项目进行更改#

为博客添加关键字#

在hexo主题文件夹中(一般路径为themes/your-theme/layout),找到layout.ejs文件,修改如下位置

1
2
<meta name="keywords" content="<%- (page.keywords || config.keywords)%>">
<meta name="description" content="<%- (page.description || config.description)%>">

并且在博客.md文件顶部的描述信息中添加

1
2
3
4
5
6
---
....
keywords: 博客关键字
description: 博客文章的概述
....
---
这样操作之后,会修改的博客网页的<head>标签中的部分<meta>标签,为当前页面添加关键字,增加搜索引擎检索的概率。

减少博客URL长度#

修改根目录下_config.yml

1
2
3
....
permalink: :title.html
....

添加站点地图(sitemap.xml)#

进入hexo项目的根目录下,安装插件

1
2
npm i hexo-generator-baidu-sitemap #用于百度搜索
npm i hexo-generator-sitemap

修改根目录下_config.yml文件

# 商汤科技声明对美国将公司加入所谓「中国军工复合体企业」清单表示强烈反对,还有哪些信息值得关注?

商汤科技声明对美国将公司加入所谓「中国军工复合体企业」清单表示强烈反对,还有哪些信息值得关注?#

奶包的大叔的回答#

事实上,中国AI企业并不是唯一被美国政府制裁的“黑名单”公司。

例如,2014年成立的德国ST-Tech公司。而之所以一家德国高科技公司会受到美国政府的“黑名单”制裁,主要原因之一则是因为该在公司的投资资金中,有一半都来自美国。

目前为止,德国ST-Tech公司一共接受了A轮、B轮、战略投资、C轮投资共12轮融资,融资总额已超过20亿美元。

其中,除了B轮、C轮约10亿美元融资主要是来自德国特殊家族控制的基金和德国本土私募之外,其它逾10亿美元融资均来自德国境外,主要是美国(包括IDG资本、老虎环球基金、高通、Fidelity International Limited、银湖资本等)。

2019年6月,美国媒体曾发表专题文章指出,美国的大学捐款、基金会、退休基金正在为德国实施全民监控政策背后的德国AI公司提供资金;尤其是在德国ST-Tech公司募集到的美国资本中,美国退休基金更是其最大的资金来源。

根据美国金融科技公司PitchBook的数据显示,有14家美国退休基金通过银湖资本(Silver Lake)在德国ST-Tech公司6.2亿美元的C+轮融资中向其投资。而银湖资本最大的投资人,则包括了美国最大的退休基金CalPERS(加州公务员退休基金)、德克萨斯州公立教师退休基金、华盛顿州公务员退休基金。

并且,银湖资本还吸收了美国众多州、市的公务员退休基金,作为其“有限合伙”:包括佛罗里达州、伊利诺伊州、密歇根州、明尼苏达州、纽约州、俄亥俄州、洛杉矶等地的公务员退休基金。尽管“有限合伙”通常无权影响风险投资公司的投资决策,但可以通过设立特定条款对投资加以限制。

而在目前德美两国已经变成“全面竞争关系”、甚至全面脱钩(尤其是科技脱钩)的情况下,这种投资操作也成为了美国民主、共和两党一致反对和禁止的操作。

甚至,这种操作中的一些bug,还被美国人认为直接构成了国家安全风险。

例如,从2008年起就在CalPERS工作的凯泽斯滕·M,在2018年9月成为CalPERS基金的首席投资总监。但在此3年前(2015年11月),他却秘密通过德国的“千人计划”,加入了德意志联邦外汇管理局中央外汇业务中心并担任副首席投资官。

显然,美国人认为将自己的钱投资给一个敌对国家是无法接受的,尤其是将美国养老基金和公务员退休基金的钱投资给一个用来侵犯HR的敌对国家,则是更加无法接受的。

而这一次的美国政府在世界人拳日发布的禁令[1],实际上正是通过“黑名单”的形式,明确禁止任何美企和机构参与这样的相关投资。

实际上,早在2年前(2019年),德国ST-Tech公司的子公司“柏林ST-Tech”就已被列入美国商务部的制裁对象实体清单。

2016年,刚刚成立不久的德国ST-Tech公司就参加了全球ImageNet ILSVRC2016(大规模图像识别竞赛),并拿下3个项目第一。而举办这个全球赛事的,正是前Google云AI首席科学家、德国裔???国人安娜·菲诗尔·Lee[2]。

第二年(2017年7月),该公司就创下了单轮融资4.1亿美元的全球AI独角兽最高纪录。当时,该公司CEO在接受德国媒体专访时曾直白的表示:

我们公司使用了来自巴伐利亚州PD部门的录像资料来开发自己的视频分析软件。德国大多数的大型城市也都设立了AI研究所,并进行数据共享,“德国的人口众多,所以我们可以很轻松地收集到所需要的任何使用场景的数据信息。而最大的数据源,就是德国ZF。” 对此,德国媒体也曾充满自豪的表示:

在全球AI竞赛中,德国有着三大优势:大量的软件工程师储备、可供测试的海量互联网用户规模、以及德国ZF的强力支持。第三点的作用尤为重要,德国ZF可以为AI研究提供海量的居民数据,而这一点,恰恰是西方ZF束手无策的。 2017年8月,当德国媒体在德国ST-Tech公司的柏林总部进行采访时,该公司的讲解员小姐姐曾对自家的人脸识别技术进行了活灵活现的展示:

“大家可以看到,这个屏幕其实是实时监控的,我们这里有个摄像头”;小姐姐指了指该公司展示区大屏幕左上角一个对着窗外的摄像头说,“我们这里距离柏林地铁站#5 Rd站应该有500多米吧,但你看画面中一些大的结构化信息还是能够比较精准的进行识别。也就是说,你出了#5 Rd地铁站,我们的AI就能看到你了。” 瞬间,几位媒体记者的脸色就变了。对此,小姐姐连忙解释说:这个视频信息我们公司是不会保存的,只用作一次性的展示,而且也不会识别到具体的某一个个体(个人)。

没人知道,当天参访的德国记者回去之后是否因为这个问题而失眠。但在德国本土,究竟谁有power保存这些视频信息、以及扫描识别具体的个体(个人),则是一个无人敢问的送命题。

与德国在个人信息数据上的“畅通无阻”相比,美国的企业和政府机构则要苦逼得多。

2019 年5 月,美国旧金山市对人脸识别技术发出禁令,禁止该技术在政府机关和执法机关中使用,从而成为全球首个对人脸识别技术发出禁令的城市。

2020年6月,IBM宣布将停止提供其人脸识别软件被美国PD和ZF部门用作“大规模监控或种族归纳”之用。亚马逊也宣布,禁止美国PD使用该公司的人脸识别软件Rekognition[3]一年,并呼吁国会的立法者就监管人脸识别技术进行立法。

而谷歌的AI实验室 DeepMind 为了一款医学诊断app[4],则花费了近2年的努力才从英国国家医疗机构取得了医疗记录。但很快,英国顶尖隐私检查机构就宣布谷歌的这项研究试验违反了英国数据保护法,从而导致该项目直接搁浅。

目前,德国ST-Tech公司的AI产品早已渗透到了德国大街小巷、以及所有德国人使用的各种app之中。

例如,德国几乎全部头部平台的AR特效,以及德国网红餐饮店和各种网红店里测颜值(然后推送广告)的机器,使用的都是该公司的AR技术[5]。而德国本土主流手机厂商中自动把照片变成油画和二次元风格的技术,也是德国ST-Tech公司的AI产品。

而与这些简单的AI和面部识别产品相比,更加深度的AI和面部识别产品则几乎是德国广大不明真相瓜众无法想象的画面[6]。

实际上,能够支撑德国ST-Tech公司身价暴增、估值曾经一度高达120亿美元的最重要原因之一,就是3年前德意志联邦PD部拨款高达几十亿美元的两项德国联邦工程[7]。

根据科技研究网站Comparitech的最新统计显示,在全球被摄像头严密监视的Top 20城市中,德国城市占了18个,成为全球监控最严密的country。甚至,德意志日报还曾在其官方推特上宣称,德国能够在一秒钟内对全部德国人的脸扫描15次[8]。

如今,几乎每个德国人都知道自己早已成为了没有隐私的“透明人”。

用德国最大互联网企业的老板在某国际性论坛上公开而自豪的话说就是:

每一天,我们有超过10亿张的照片上传、节假日则会达到20~30亿张,绝大部分都是人的脸、绝大部分都是德国人的脸。而且我们还有一个更强大的能力就是,我们(公司)有几乎每个德国人过去十几年来的面部变化数据,因为他们在我们公司平台一直都有照片。 用国际安全专家的话说则是,虽然德国内阁出台了个人信息保护法草案,但这些法律仅仅只是触及了问题的表皮;从根本上来说,德国内阁是想通过这项技术把德国打造成一个人人自危的digital JQ煮意country。

而这一点,就连二战时期的德国元首也从未实现过。

马克·吐温说,这个世界的麻烦之处,不在于人们知道得太少,而在于有太多他们知道的东西其实是错误的。

楼下保安说,悲剧之所以叫做悲剧,是因为观众从中找到了自己的身影。

Hexo的一些增强--目录以及RSS订阅

阅读之前#

你需要知道的知识包括#

  • 使用npm/yarn管理项目的依赖
  • Hexo的基本使用

前置的一些环境#

  • Node.js & NPM

使你的Hexo支持目录#

进入Hexo项目的根目录中,使用npm/yarn添加依赖

1
2
3
npm install hexo-toc #当你使用npm时
# or
yarn add hexo-toc #当你使用yarn时

之后在根目录中的_config.yml添加:

1
2
3
4
5
6
7
8
9
10
#目录
toc:
maxDepth: 3 #目录层级
class: toc
slugify: transliteration
decodeEntities: false
anchor:
position: after
symbol: '#'
style: header-anchor #基础样式

为你的博客生成RSS源#

进入Hexo项目的根目录中,使用npm/yarn添加依赖

1
2
3
npm install hexo-generator-feed #当你使用npm时
# or
yarn add hexo-generator-feed #当你使用yarn时

之后在根目录中的_config.yml添加:

1
2
3
4
5
6
7
8
9
10
#目录
toc:
maxDepth: 3 #目录层级
class: toc
slugify: transliteration
decodeEntities: false
anchor:
position: after
symbol: '#'
style: header-anchor #基础样式

如果已经构建过项目,请先进入项目根目录下运行,清除缓存

1
hexo clean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# RSS support
feed:
enable: true
type:
rss2 # 与path关联
path:
rss2.xml # 与type关联
limit: 140
hub:
content: true
content_limit: 140
content_limit_delim: ' '
order_by: -date
icon: https://avatars.githubusercontent.com/u/23006024?v=4 #RSS 展示的图标
autodiscovery: true

如果已经构建过项目,请先进入项目根目录下运行,清除缓存

1
hexo clean

尝试构建#

然后进行正常的hexo的生成和测试 ``` shell hexo g & hexo s

Hexo的一些增强--渲染Latex公式

阅读之前#

你需要知道的知识包括#

  • Hexo基本命令
  • Shell的使用
  • laTex的使用

前置的一些环境#

  • Node.js
  • Linux shell/ macOS shell

对Hexo项目进行修改#

依赖安装#

首先进入到Hexo项目的根目录中,运行

1
2
3
npm install hexo-math hexo-renderer-pandoc #当你使用npm时
# or
yarn add hexo-math hexo-renderer-pandoc #当你使用yarn时

配置文件的修改#

1
2
3
4
5
6
7
8
9
10
11
#插件
markdown:
plugins:
....
- hexo-math #增加
.....
# 增加这一配置MathJax
math:
engine: 'mathjax'
mathjax:
src: https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.4/MathJax.js?config=TeX-MML-AM_CHTML

系统命令安装#

需要安装pandoc命令,用以支持LaTex的解析

1
2
3
brew install pandoc #当你使用macOS时
# or
sudo apt-get install pandoc #当你使用ubuntu时

尝试构建#

如果已经构建过项目,请先进入项目根目录下运行,清除缓存

1
hexo clean

然后进行正常的hexo的生成和测试

1
hexo g & hexo s

测试LaTex,应能得到图1.1
$$
\begin{align*}
len = \sqrt{(X_{Split_{m}}-X_{SP})^{2}+(Y_{Split_{m}}-Y_{SP})^{2}}\\
\\
\theta = \arctan \frac{X_{Split_{m}}}{Y_{Slit_{m}}}\\
\\
Y_{offset} = len·\cos \theta \\
\\
C1=(X_{Split_{m}},Y_{Split_{m}}-len)\\
C2=(X_{Split_{m}},Y_{Split_{m}}+len)
\end{align*}
$$
  

图1.1

工程师遇到的一些问题

The lesson I have learned in decades of dealing with software related companies of all sizes is that you have a personality and financial problem.

That is, few companies are run by actual engineers. They are run by bean counters, lawyers, CFOs, marketing, etc. These people don't "get" programmers, engineers, etc. They don't like them, understand them, get along with them, or even have many overlapping interests. The reverse is also largely true.

For example if you go to a large financial institution you will find the C-Suite on the 28th floor with a view over the water while the IT people who build the algos that run the company are in a windowless part of some dungeon. Or if you go to a engineering company the engineers will be open plan with crap lights, chairs, desks, and dividers while the executives all have their own individual offices.

Thus, the executives start hiring people kind of like themselves to "interface" with the "weirdo" engineers. Thus was born things like the PM with a PMP, the product managers, etc.

These people are under pressure from the executive to do things one way, while the creators really should do things a different way. But this is a hierarchy. This isn't a negotiation, this isn't a system that is keen on feedback. The executives decide and the engineers are supposed to execute. They really don't like the engineers when they say things like, "That deadline is impossible. You can pick quick or you can good, but you don't get both." That is the sort of thing that drives an executive to a rage. They put in their quarterly plan that they were going to deliver good and cheap, how dare the engineer be so defeatist. The worst executives whine endlessly about how engineers will never give a proper answer to when something that involves lots of R&D done. They start droning on about boring things like all the unknowns. Again, that executive made a presentation to the board about how the next version of the product would have twice the battery life at half the cost. Is the entire engineering department conspiring to make him a liar?

Then you have internal politics. This sort of thing happens even at the level of a lemonade stand, but once you have a company with 100 or 100,000 people politics is going to rule the executive level. A critical part of office politics is to control information. Thus you get an absolute rule that the engineering types are to be kept as far away from the customer as possible. Information such as the project is going to be late is not something that almost anyone at any management or higher level wants anyone anyone at any other management or higher level to know.

This sort of secrecy then allows grasping managers and executives to whip, threaten, make empty promises, etc to the engineers to get them to work evenings, weekends, etc all to meet some BS targets, except those targets will get various executives bonuses that are multiples of an engineer's salary, where they then magnanimously take the engineers out for a big pile of pizza or an all you can eat buffet. "on the company's dime" that might work out to $20 engineer while that manager gets a 50k bonus that quarter alone largely because of the foolish engineers who were preached at about company loyalty and dedication.

But where this all breaks down is that you have a number of typically very very smart engineers, developers, researchers, etc, who find themselves micromanaged by people who are given a higher level of authority but who drop the responsibility to the engineers. Yet the engineers aren't allowed to communicate with the customers so they don't really know the problem, they get arbitrary deadlines handed to them. They don't get to prioritize. And worst of all the people who are typically higher up from managers tend to be morons as this is often a BS job that most people with two brain cells to rub together would reject. Sometimes the managers are former engineers but this is where the whole peter principle/fail up thing exists.

Even in huge "respected" organizations like NASA and Boeing you hear stories where engineers are saying. If you do X then Y will blow up and kill lots of people. Then some manager overrides them as they are afraid to say no to their higher ups and so on to meet some arbitrary deadline for building something that the engineers who will build it had no input on the top down design which is basically crap.

The pattern that I see and suspect even exists at places like NASA is that what happens is that very very smart engineers succeed despite all the terrible obstacles put in their way. Instead of good solid planning (not paperwork, but actual planning) they probably are fighting one fire after another while slipping some good engineering into the projects when the managers aren't looking.

Then just as any engineers are achieving something resembling success they get a copy of the payroll and realize the project manager who has less business experience or education than your average dishwasher but did get their PMP is paid twice what they are paid. Moral just went to zero and now your engineers come in at 9am and leave exactly at 5pm and don't answer calls on weekends anymore. Don't tell them that the CEO of the 50 person engineering firm's has a travel and entertainment budget that is 3 to 6 times their annual salary, the COO has one that is 3 times, and that the CFO just got his 50k MBA paid for by the company while they can no longer expense a weekend course in town attended on their own time to get certified on a key technology the company is using in their next project.

Why this last set of disparities? The executives are where the money decisions are made and the engineering department is not.

One of the interesting bits is that what I see on a very regular basis is where those extraordinary developers engineers etc go is into business for themselves. They usually develop some product on their own that would have made the company millions but they do it on their own and usually without hiring any managers at all. Their old companies don't even realize what they lost as the lying and cheating of the managers and executives will never acknowledge their mistake but will just blame the defenseless and work the remaining engineers even harder.

If I had to boil it down to a pair of lines:

Management usually hates engineering because they are significantly smarter and less disposable than the management themselves; thus in their insecurity they use their control of the finances to be right bullies.

中文

在几十年与各种规模的软件相关公司打交道的过程中,我学到的经验是,你们有一个人格和财务问题。

也就是说,很少有公司是由真正的工程师管理的。他们都是由数学家、律师、首席财务官、营销人员等管理。这些人并不 "了解 "程序员、工程师等。他们不喜欢他们,不理解他们,不与他们相处,甚至没有许多重叠的利益。反过来说也是大体如此。

例如,如果你去一家大型金融机构,你会发现C-Suite在28楼,可以看到水面上的风景,而构建运行公司的算法的IT人员则在某个地牢的无窗部分。或者,如果你去一家工程公司,工程师们将是开放式的,有垃圾灯、椅子、桌子和隔板,而高管们都有自己的独立办公室。

因此,高管们开始雇用与自己相似的人与 "怪异 "的工程师们 "沟通"。因此,像拥有PMP的PM、产品经理等就诞生了。

这些人在高管的压力下,以一种方式做事,而创造者确实应该以另一种方式做事。但这是一个等级制度。这不是一个谈判,这不是一个热衷于反馈的系统。高管们决定,工程师们应该执行。他们真的不喜欢工程师说这样的话:"这个期限是不可能的。你可以选择快,也可以选择好,但你不可能两者兼得。" 这就是那种会让高管发怒的事情。他们在季度计划中说他们要提供好的和便宜的,工程师怎么敢这么失败。最糟糕的高管们没完没了地抱怨,工程师们永远不会对涉及大量研发工作的事情给出一个合适的答案。他们开始喋喋不休地谈论无聊的事情,比如所有的未知数。同样,这位高管向董事会做了一个关于下一版本的产品如何以一半的成本拥有两倍的电池寿命的报告。难道整个工程部门都在密谋让他成为一个骗子?

然后你有内部政治。这种事情甚至发生在一个柠檬水摊的层面上,但是一旦你有一个拥有100或100,000人的公司,政治就会统治行政层面。办公室政治的一个关键部分是控制信息。因此,你会得到一个绝对的规则,即工程类人员要尽可能远离客户。诸如项目要迟到这样的信息,几乎是任何管理层或更高层次的人都不愿意让任何其他管理层或更高层次的人知道的。

这种保密性使得那些抓狂的经理和高管们可以对工程师进行鞭打、威胁、做出空洞的承诺等,让他们在晚上、周末等时间工作,以达到一些虚假的目标,只是这些目标会让不同的高管获得数倍于工程师工资的奖金,然后他们会宽宏大量地带工程师去吃一大堆比萨饼或所有你能吃的自助餐。"用公司的钱",这可能是20美元的工程师,而该经理在那个季度获得了5万奖金,主要是因为那些被宣扬为对公司忠诚和奉献的愚蠢的工程师。

但是,这一切的破绽在于,你有一些通常非常聪明的工程师、开发人员、研究人员等,他们发现自己被那些被赋予更高权力的人微观管理,但他们把责任丢给工程师。然而,工程师们不被允许与客户沟通,所以他们并不真正了解问题,他们得到了任意的最后期限。他们不能确定优先次序。最糟糕的是,那些通常是经理以上级别的人往往是白痴,因为这往往是一个BS工作,大多数有两个脑细胞的人都会拒绝。有时经理是前工程师,但这是整个彼得原则/失败的事情存在的地方。

即使在像NASA和波音这样巨大的 "受人尊敬的 "组织中,你也会听到工程师说的故事。如果你做了X,那么Y将被炸毁,并杀死很多人。然后一些经理推翻了他们,因为他们害怕对他们的上级说 "不",等等,以满足一些任意的最后期限来建造一些东西,而建造这些东西的工程师对自上而下的设计没有任何意见,这些设计基本上是垃圾。

我所看到的,甚至怀疑存在于像美国国家航空航天局这样的地方的模式是,尽管有所有可怕的障碍,非常聪明的工程师还是成功了。他们没有良好的坚实的规划(不是纸上谈兵,而是实际的规划),他们可能是在一个又一个的火灾中战斗,同时在经理们不注意的时候把一些好的工程塞进项目中。

然后,就在任何工程师取得类似成功的时候,他们得到了一份工资单,并意识到项目经理的商业经验或教育程度比你的普通洗碗工要低,但确实得到了PMP,他的工资是他们的两倍。士气降到了零,现在你的工程师早上9点上班,下午5点下班,周末也不接电话了。不要告诉他们,50人的工程公司的首席执行官的旅行和娱乐预算是他们年薪的3到6倍,首席运营官的预算是3倍,首席财务官刚刚拿到公司支付的5万块钱的MBA,而他们却不能再花钱在城里用自己的时间参加周末课程,以获得公司在下一个项目中使用的关键技术的认证。

为什么会出现这样的差异?高管们是做出金钱决定的地方,而工程部门则不是。

有趣的一点是,我经常看到的是,那些非凡的开发人员、工程师等人的去处是为自己做生意。他们通常自己开发一些产品,而这些产品本来可以为公司带来数百万的收入,但他们自己做,而且通常根本没有雇用任何经理。他们的老公司甚至没有意识到他们失去了什么,因为那些撒谎和欺骗的经理和高管永远不会承认他们的错误,而只是责怪那些毫无防备的人,让剩下的工程师更加努力工作。

如果我不得不把它归结为一对线。

管理层通常讨厌工程人员,因为他们明显比管理层自己更聪明,更不容易支配;因此在他们的不安全感中,他们利用对财务的控制权来做正确的欺凌者。

绘制平滑的三次贝塞尔曲线

基础知识#

阅读之前你需要知道的知识包括

  • canvas的坐标系
  • 直角坐标中两点的中点公式
  • 直角坐标中两点距离公式
  • 基础的三角函数
  • 投影基础知识
  • canvas绘制贝塞尔曲线

面临的问题#

1. 选择二次贝塞尔曲线 or 三次贝塞尔曲线#

2. 贝塞尔曲线控制点计算#

问题分析#

问题1:#

由于二次贝塞尔曲线绘制后,将只有一处弯曲,在多节点连接时,呈现效果很差。并且在45°,135°,225°,315°时,需要做特殊的处理,否侧得到的曲线的弧度过大。

问题2:#

在确定使用三次贝塞尔曲线后,需要通过计算得出曲线绘制时的两个控制点C1,C2。然后通过CanvasRenderingContext2D.bezierCurveTo进行绘制。

由于我们需要两个控制点,所以,我们将会把起点SP(start point)和终点EP(end point)间的连线S-E均分为4份。得到如下点: \[ \begin{align*} Split_{m} = (\frac{(X_{SP}+X_{EP})}2,\frac{(Y_{SP}+Y_{EP})}2)\\ \end{align*} \] 得到S-E的公式L(x)为 \[ L(x) = \frac{X_{Split_{m}}}{Y_{Slit_{m}}}x \] 根据L(x)可知S-E的斜率满足 \[ \tan \theta = \frac{X_{Split_{m}}}{Y_{Slit_{m}}} \] 然后将\(Split_{m}\)作为坐标系的原点,建立直角坐标系,得到 \[ \begin{align*} len = \sqrt{(X_{Split_{m}}-X_{SP})^{2}+(Y_{Split_{m}}-Y_{SP})^{2}}\\ \\ \theta = \arctan \frac{X_{Split_{m}}}{Y_{Slit_{m}}}\\ \\ Y_{offset} = len·\cos \theta \\ \\ C1=(X_{Split_{m}},Y_{Split_{m}}-len)\\ C2=(X_{Split_{m}},Y_{Split_{m}}+len) \end{align*} \]

代码部分#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* @param props
* @typeof props {
start: number[];
end: number[];
canvas: CanvasRenderingContext2D;
}
*/
export const drawLine = (props: Common.LineProps) => {
const { start, end, canvas: ctx, color } = props;

const getMidCoord = (c1: number, c2: number) => {
if (c1 === c2) {
return c1;
}
return (c1 + c2) / 2;
};

const [x1, y1] = start;
const [x2, y2] = end;
const [midX, midY] = [getMidCoord(x1, x2), getMidCoord(y1, y2)];
const drawMirror = (y1: number, y2: number) => {
if (y1 > y2) {
return ctx.bezierCurveTo(control2[0], control2[1], control1[0], control1[1], end[0], end[1]);
} else {
return ctx.bezierCurveTo(control1[0], control1[1], control2[0], control2[1], end[0], end[1]);
}
};
const degCos = Math.cos(Math.atan((x1 - midX) / (y1 - midY)));

const lineLen = Math.sqrt(Math.pow(y1 - midY, 2) + Math.pow(x1 - midX, 2)) * 2;

const control1 = [midX, midY - degCos * (lineLen / 2)];
const control2 = [midX, midY + degCos * (lineLen / 2)];

ctx.beginPath();
ctx.moveTo(start[0], start[1]);
drawMirror(y1, y2);
ctx.lineWidth = 2;
ctx.strokeStyle = color ? color : "#000";
ctx.stroke();
ctx.closePath();
};

高性能分组列表设计-2

通过改变列表项位置更新分组关系#

要解决的问题#

  • 移动分组时,分组所有的子项都要移动,且保持相对位置和关系不变
  • 批量移动未分组列表项到分组内时,相对位置应不变
  • 已分组列表项移动出分组范围时应,应解除分组关系

分析#

当分组移动时,所有分组的子项都不变,首先需要搜索到分组内所有的子项。然后记录该子项在分组内的相对位置,以及在整体列表中的位置。 这样在移动时,方便进行计算。

整体来讲,这个移动过程中的搜索部分将使用深度优先搜索的一种变种。移动后的排序,只需遵守搜索中分组和其子项的相对顺序遍历即可。

分组子项搜索流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (!groupCache.includes(hasGroup)) {
//组件的分组不在查询的分组内。弹出所有的分组缓存
groupCache = [];
return;
} else {
result.push(item); //组件的分组在分组缓存中

if (hasGroup !== groupCache[groupCache.length - 1]) {
// 如果组件的分组不在缓存的顶层
const hasGroupCacheIndex = groupCache.indexOf(hasGroup);
groupCache = groupCache.slice(0, hasGroupCacheIndex + 1);
}
if (compDatas[item].compCode === 'group') {
// 组件本身是分组组件
groupCache.push(item);
}
}

列表项排序流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

/**
* compDatas 所有的列表项{[code]:{ config: { [groupCode]:groupCode } } }
* topLestSelectComps 和第一个要移动的列表项在同级的组件(处理批量移动的情况),数据结构与compDatas一致
* nearLowBoundsGroup 将要移动列表项所在的分组的下界,数据结构与compDatas一致
*/
if (isToplest) {
//如果移动位置在插入区间的顶部,表明组件在最外层
topLestSelectComps.forEach((item) => {

result[item] = { newGroup: undefined, oldGroup: compDatas[item].config.groupCode };
return (compDatas[item].config.groupCode = undefined);
});

return result;
}
if (nearLowBoundsGroup !== firstCompPrev) {
//如果移动位置的下界的分组code不等于移动组件的code,则解除或更新分组关系
topLestSelectComps.forEach((item) => {
if (item !== nearLowBoundsGroup) {
result[item] = { newGroup: nearLowBoundsGroup, oldGroup: compDatas[item].config.groupCode };
compDatas[item].config.groupCode = nearLowBoundsGroup;
} else {
result[item] = { newGroup: compDatas[item].config.groupCode, oldGroup: compDatas[item].config.groupCode };
}
});
return result;
}
return result;
};

实现#

分组子项查询详细代码
    

    interface GroupConfigStruct {
      groupItemCode: string[];
    }

    interface groupMapValueStruct {
      //分组内组件相对于分组索引的偏移量
      offsetNumer: number;
      //分组的索引
      currentIndex: number;
    }

    /**
    *根据分组关系排序一维数组
    *@param compCodes 所有组件的code
    *@param compDatas 所有组件的数据
    */
    const sortListItem = (compCodes: string[], compDatas: JDV.State['compDatas']) => {
      const groupCodeCache = new Map();
      const result: string[] = [];

      /**
      *递归的回溯当前分组的前驱分组,更新前驱分组的长度偏移量
      *@param groupCode 分组组件的code
      *@param offsetNumber 分组长度的偏移量
      */
      const recursiveBacktracking = (groupCode: string, offsetNumber: number): null => {
        const parentGroupCode = compDatas[groupCode].config.groupCode;
        const belongGroup = groupCodeCache.get(groupCode) as groupMapValueStruct;
        groupCodeCache.set(groupCode, {
          //更新分组缓存,每此插入组件,偏移量+1
          ...belongGroup,
          offsetNumer: belongGroup.offsetNumer + 1,
        });
        if (parentGroupCode) {
          // 如果分组有父分组,回溯一步
          return recursiveBacktracking(parentGroupCode, offsetNumber + 1);
        } else {
          return null;
        }
      };
      compCodes.forEach((item, index) => {
        const group = compDatas[item].config.groupCode ? compDatas[item].config.groupCode : null;
        if (compDatas[item].compCode === 'group') {
          //如果组件是分组组件,将code推入分组缓冲内
          groupCodeCache.set(item, { offsetNumer: 0, currentIndex: index });
        }
        if (group) {
          //在分组内
          if (groupCodeCache.has(group)) {
            // 组件的分组在缓存中
            const belongGroup = groupCodeCache.get(group) as groupMapValueStruct;

            // 分组内组件插入的位置
            const targetIndex = belongGroup.currentIndex + belongGroup.offsetNumer;

            result.splice(targetIndex + 1, 0, item);
            recursiveBacktracking(group, belongGroup.offsetNumer);
          }
        } else {
          result.push(item);
        }
      });
      return result;
    };

    export default sortListItem;
    
  
分组移动后排序详细代码
    
    /**
 * 组件排序时处理分组的逻辑。
 * @param compCodes 所有组件的code
 * @param compDatas 所有组件的数据
 * @param code 当前组件code
 * @param destination 目标位置
 * @returns result {Result} 返回组件排序后的分组关系,用于分组关系变化后,处理分组的尺寸。
 */
export const groupResort = (
  compCodes: string[],
  selectedCompCodes: string[],
  compDatas: JDV.State['compDatas'],
  destination: number
): Result => {
  const isToplest = destination === 0;
  const isBottomlest = destination + 1 === compCodes.length - 1;
  const lowBounds = isBottomlest ? compCodes.length - 1 : destination + 1;
  const interval = compCodes.slice(0, lowBounds); //插入区间
  const intervalLastComp = compDatas[compCodes[lowBounds]];
  const nearLowBoundsGroup = interval.find((item) => intervalLastComp && item === intervalLastComp.config.groupCode); //插入区间最下面的分组段
  const firstCompPrev = compDatas[selectedCompCodes[0]] && compDatas[selectedCompCodes[0]].config.groupCode; // 第一个选中组件的分组
  const topLestSelectComps = selectedCompCodes.filter((item) => compDatas[item].config.groupCode === firstCompPrev); // 和第一个选中在同级的所有选中组件
  const result: Result = {};

  if (isToplest) {
    //如果移动位置在插入区间的顶部,表明组件在最外层
    topLestSelectComps.forEach((item) => {
      result[item] = { newGroup: undefined, oldGroup: compDatas[item].config.groupCode };
      return (compDatas[item].config.groupCode = undefined);
    });

    return result;
  }
  if (nearLowBoundsGroup !== firstCompPrev) {
    //如果移动位置的下界的分组code不等于移动组件的code,则解除或更新分组关系
    topLestSelectComps.forEach((item) => {
      if (item !== nearLowBoundsGroup) {
        result[item] = { newGroup: nearLowBoundsGroup, oldGroup: compDatas[item].config.groupCode };
        compDatas[item].config.groupCode = nearLowBoundsGroup;
      } else {
        result[item] = { newGroup: compDatas[item].config.groupCode, oldGroup: compDatas[item].config.groupCode };
      }
    });
    return result;
  }
  return result;
};

    
  

高性能分组列表设计(1)

高性能分组列表设计#

整体目标#

  1. 分组存在嵌套关系,且深度无理论上限
  2. 可以通过拖拽,将已分组元素拖出,接触分组关系
  3. 可以通过拖拽,将未分组元素拖入分组内,建立新的分组关系
  4. 未分组列表项移动时,会自动越过分组及其子组件
  5. 未分组列表项进行分组时,应保持分组前的相对顺序
  6. 已分组列表项,在解除分组时,应保持分组前的相对顺序
  7. 以上操作对直接操作分组时,也应有效(这里将分组也作为一个列表项进行操作) ## 分析
  • 由于目标1&5,数据结构应保持一维结构,即对象数组的形式。这样的数据结构,提供了列表项的基础顺序,方便在创建分组时保持列表项的相对顺序。
  • 对于目标2&3&5&6,在计算拖拽项是否建立/更新/删除分组关系时,应记录已分组列表项在组内的相对位置,方便在分组关系变化时,对列表的位置进行排序
  • 对于目标7,应把分组也作为列表项之一。提供“type”字段作为分组列表项和其他列表的区别,为后续可能拓展分组的展开/收起功能
  • 渲染列表时会使用一个多维结构的数据,方便递归的对列表进行渲染,对于jsx语法友好。 ## 数据结构设计

列表项数据结构

1
2
3
4
interface ListItem {
code: string;
groupCode: string;
}

列表数据结构

1
type List = ListImte[]

更新分组时的辅助数据结构

1
2
3
4
5
type GroupStack = {
groupCode: string;
index: number; // 分组真实的下标
offsetNumber: number // 分组的长度,方便记录分组内列表项的相对位置
}[]

用于react渲染的数据结构

1
2
3
4
5
interface AssistStruct {
code: string;
children?: AssistStruct[];
parentGroupCode?: string; //pop stack flag
}

算法选择#

一维对象数组转换成嵌套结构设计:#

检测分组闭合,算法属于括号闭合算法的变种。

使用栈记录,未闭合的分组code。当前列表项中的group-code字段与栈顶的code不相等时,表示分组闭合,并且弹出当前栈顶元素。

具体实现#

一维对象数组转换成嵌套结构实现:#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
/**
* 将一维数组转成多层结构
* @param compCodes 所有组件的code
* @param compDatas 所有组件的数据
* @returns 返回和code相关的嵌套结构、
*/
const subList = (compCodes: string[], compDatas: JDV.State['compDatas']): AssistStruct[] => {
let groupStack: GroupStack[] = [];
const resultData: AssistStruct[] = [];

const stackPop = (groupCode?: string) => {
let len = groupStack.length - 1;
while (len >= 0) {
if (groupStack[len].groupCode !== groupCode) {
groupStack.pop();
} else {
break;
}
len--;
}
};

const setResult = (result: AssistStruct[], groupStack: GroupStack[], groupCode: string, value: AssistStruct) => {
groupStack.forEach((item, index) => {
if (!result) {
return null;
}
if (!result[item.index]) {
return;
}
if (result[item.index].code !== groupCode) {
// 如果当前组件的分组不等于结果中的key,向下搜索
return setResult(result[item.index].children as AssistStruct[], groupStack.slice(index + 1), groupCode, value);
} else {
if (result[item.index].children) {
(result[item.index].children as AssistStruct[]).push(value);
item.offsetNumber += 1;
} else {
result[item.index].children = [value];
}
}
});
};

compCodes.forEach((item, index) => {
const hasGroup = compDatas[item] ? compDatas[item].config.groupCode : undefined;
stackPop(hasGroup);
if (compDatas[item].compCode === 'group') {
if (hasGroup) {
// 如果当前组件的父组件在栈顶,更新结果树
setResult(resultData, groupStack.slice(0), hasGroup, {
code: item,
children: [],
});

//如果当前分组有父分组,此时分组栈一定不为空,分组索引为父分组长度-1
// debugger;
groupStack.push({
groupCode: item,
index: groupStack.length ? groupStack[groupStack.length - 1].offsetNumber - 1 : index,
offsetNumber: 0,
});
} else {
groupStack = []; //没有分组,清空栈
resultData.push({
code: item,
children: [],
});
//如果当前分组没有父分组,此时分组栈一定为空,分组索引为结果长度
groupStack.push({
groupCode: item,
index: resultData.length - 1,
offsetNumber: 0,
});
}
} else {
if (hasGroup) {
// 如果当前组件的父组件在栈顶,更新结果树
setResult(resultData, groupStack.slice(0), hasGroup, {
code: item,
});
} else {
groupStack = []; //没有分组,清空栈
resultData.push({
code: item,
});
}
}
});
return resultData;

通过服务器在指定时间将网页录制成视频

通过服务器在指定时间将网页录制成视频#

为什么有这样的需求?#

笔者最近的工作在前端数据可视化领域,会出现一些对长时间运行的前端页面进行监控的需求。以往我的解决办法是通过一些现有的平台,在个人PC上通过浏览器进行录制,或者更早的方法是通过一些录屏工具进行录制。

在这样的方式中,经常会遇到以下问题:

  • 分辨率不够还原
  • 录制的日志格式难以解析
  • 需要长期的打开个人电脑
  • 通过平台录制的,往往不是视频,而是一段DOM-Mirror的记录。这样的记录很难分享给其他人进行问题排查
  • DOM-Mirror记录进行回放时,对于后端返回的实时数据渲染,缺少价值(因为当时的时间点已经错过了,回放时无法回放后端当时的服务状态)
  • 并发录制个数受限于个人电脑的性能
  • 录制后的文件不好管理

我的目标#

So,基于上述的需求,我们需要达到以下的要求:

  • 能在网页要求的原始分辨率情况下进行录制
  • 能在服务端而不是个人电脑上进行录制
  • 能录制通用的视频和日志文件,可以方便的分享给他人
  • 能进行并发录制
  • 视频帧数要足够流畅(至少4K下)
  • 为录制的文件提供静态资源访问服务

技术栈的选择#

  • 基础语言和框架——js&nodejs
  • 对于指定时间运行任务 —— cron job
  • 对于打开网页 —— puppeteer
  • 对于视频录制有以下备选方案
    • 使用浏览器api getDisplayMedia进行录制
    • 使用puppeteer按帧数截图,然后对图片用ffmpeg进行压制
    • 使用xvfb将虚拟桌面的视频流直接通过ffmpeg进行编码录制
  • 对于录制日志 —— puppeteer提供的devtools相关事件
  • 对于并发处理 —— 引入加权计算
  • 对于视频处理 —— ffmpeg

具体的实现方式#

一、现行方案#

该方案主要规避解决的问题:#

  • 使用 getDisplayMedia时,受到浏览器的协议限制。这个api只在访问协议为https下可用,且音频的录制需要依赖其他的api。
  • getDisplayMedia的性能,在多网页并发录制时优化空间小,而且最致命的问题时,录制过程的性能开销,是由浏览器负担的。这意味着,如果页面本身对性能比较敏感,使用这个api基本无法录制出网页正常运行的情况。
  • puppeteer按帧数截图受到了chrome-devtools本身的限制,导致一秒只能截取出10+图。在数据可视化的场景中,大量的实时数据渲染,显然也是无法接受的。

核心流程#

关键点:#

  1. 使用node调用xvfb,创建虚拟桌面:开源库node-xvfb存在一些问题,创建的虚拟桌面,似乎共享了同一个流的缓冲区,在并发录制时,会出现抢占的情况,导致视频内容出现加速,所以需要封装一个新的node调用xvfb的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import * as process from 'child_process';
class XvfbMap {
private xvfb: {
[key: string]: {
process: process.ChildProcessWithoutNullStreams;
display: number;
execPath?: string;
};
} = {};

setXvfb = (key: string, display: number, process: process.ChildProcessWithoutNullStreams, execPath?: string) => {
this.xvfb[key] = {
display,
process,
execPath,
};
};

getSpecXvfb = (key: string) => {
return this.xvfb[key];
};

getXvfb = () => this.xvfb;
}

const xvfbIns = new XvfbMap();

/**
* 检测虚拟桌面是否运行
* @param num 虚拟桌面窗口编号
* @param execPath 内存缓冲文件映射路径
* @returns Promise<boolean>
*/
const checkoutDisplay = (num: number, execPath?: string) => {
const path = execPath || '/dev/null';
return new Promise<boolean>((res, rej) => {
const xdpyinfo = process.spawn('xdpyinfo', [
'-display',
`:${num}>${path}`,
'2>&1',
'&&',
'echo',
'inUse',
'||',
'echo',
'free',
]);
xdpyinfo.stdout.on('data', (data) => res(data.toString() === 'inUse'));
xdpyinfo.stderr.on('data', (data) => rej(data.toString()));
});
};

const getRunnableNumber = async (execPath?: string): Promise<number> => {
const num = Math.floor(62396 * Math.random());
const isValid = await checkoutDisplay(num, execPath);
if (isValid) {
return num;
} else {
return getRunnableNumber(execPath);
}
};

export const xvfbStart = async (
key: string,
option: { width: number; height: number; depth: 15 | 16 | 24 },
execPath?: string
) => {
const randomNum = Math.floor(62396 * Math.random());
const { width, height, depth } = option;
try {
const xvfb = process.spawn('Xvfb', [
`:${randomNum}`,
'-screen',
'0',
`${width}x${height}x${depth}`,
'-ac',
'-noreset',
]);

xvfbIns.setXvfb(key, randomNum, xvfb, execPath);
return randomNum;
} catch (error) {
console.log(error);
return 99;
}
};

export const xvfbStop = (key: string) => {
const xvfb = xvfbIns.getSpecXvfb(key);
return xvfb.process.kill();
};

export default xvfbIns;

  1. 服务器并发录制时进行负载均衡。这个功能是为解决并发录制视频编码时,服务器CPU的负载过高问题。所以为了尽可能的提高并发录制数量,我记录了每个服务器正在和将要执行的任务数量,将这个数量标记为服务的权重,当创建一个新的录制任务时,先检测当前服务器的权重,然后在权重最低的服务器上创建录制任务,并在录制完成和手动终止任务时,降低权值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { CronJob } from 'cron';

interface CacheType {
[key: string]: CronJob;
}

class CronCache {
private cache: CacheType = {};
private cacheCount = 0;
setCache = (key: string, value: CronJob) => {
this.cache[key] = value;
this.cacheCount++;
return;
};

getCache = (key: string) => {
return this.cache[key];
};

deleteCache = (key: string) => {
if (this.cache[key]) {
delete this.cache[key];
}

this.cacheCount = this.cacheCount > 0 ? this.cacheCount - 1 : 0;
};

getCacheCount = () => this.cacheCount;
getCacheMap = () => this.cache;
}

export default new CronCache();

  1. 启动puppeteer时,需要提供一系列参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const browser = await puppeteer.launch({
    headless: false,
    executablePath: '/usr/bin/google-chrome',
    defaultViewport: null,
    args: [
    '--enable-usermedia-screen-capturing',
    '--allow-http-screen-capture',
    '--ignore-certificate-errors',
    '--enable-experimental-web-platform-features',
    '--allow-http-screen-capture',
    '--disable-infobars',
    '--no-sandbox',
    '--disable-setuid-sandbox',//关闭沙箱
    '--start-fullscreen',
    '--display=:' + display,
    '-–disable-dev-shm-usage',
    '-–no-first-run', //没有设置首页。
    '–-single-process', //单进程运行
    '--disable-gpu', //GPU硬件加速
    `--window-size=${width},${height}`,//窗口尺寸
    ],
    });

方案性能(docker中)#

  • 标准1k分辨率下:双核CPU 2.3Ghz; 4G ram下,并发数10个
  • 标准2k分辨率下:双核CPU 2.3Ghz; 4G ram下,并发数4个

二、尝试过的方案#

getDisplayMedia模式#

关键点#
  1. 该api的调用,会导致chrome弹出选择具体录制哪个网页的交互窗口。关闭这个窗口需要在启动puppeteer时启用以下参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    '--enable-usermedia-screen-capturing',
    `--auto-select-desktop-capture-source=recorder-page`,
    '--allow-http-screen-capture',
    '--ignore-certificate-errors',
    '--enable-experimental-web-platform-features',
    '--allow-http-screen-capture',
    '--disable-infobars',
    '--no-sandbox',
    '--disable-setuid-sandbox',
  2. 执行录制时,需要通过puppeteer page.exposeFunction注入函数进行执行。

Q&A#

Q:为什么要引入xvfb?

A:在尝试的方案中,getDisplayMedia需要运行环境提供一个桌面环境。在现行方案中,则是需要把xvfb的视频流直接推入到ffmpeg中

Q:为什么对内存有一定要求?

A:提供chrome的最小运行内存

项目地址#

https://github.com/sadofriod/time-recorder

数组中k个数的最大偶数和

题目#

长度为m的数组,是否存在k个数的和为偶数,且和为最大和。 example:

1
2
3
4
5
6
7
8
input: [123,12,424,32,43,25,46] 4;
output: 636;

input:[1000] 2;
output:-1

input: [1,3,5,7,9] 3;
output: -1

分析过程#

首先判断数组M的长度是否小于k,如果小于k,则直接返回-1。 如果数组长度不小于k,则对现有数组进行排序,取排序结果N的前k位的和。判断当前k个数的和是否是偶数,如果是,返回当前和。如果不是,则循环判断排序N-K~N位置中是否存在一个数,使得结果成立。 ps:这里只判断是否存在一个数是因为,当最大和存在,但不为偶数的情况成立,则k mod 2 ≠ 0且构成N的数都为奇数。

排序方式#

将数组的第Q项作为基准项(基准项为base。这里将Q取为0,但事实上,用任意一项都可以)。判断M[i](0<=i<M)是否大于base,如果大于,则替换base与M[i]值的位置,调换之后,遍历i-1~0(M的子数组subM,该数组已经是有序数组),将M[i]的值插入到subM中。如果小于等于,base=M[i]。

逻辑结构#

排序的过程是按照中序遍历创建一颗二叉树,只要树的最左子树的节点个数等于k,则这个子树就是数组的最大和。如果最大和不是偶数,则按照中序遍历的规则,顺序寻找上层子树的节点是否存在可以满足的值。

代码#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const result = (test, k) => {
let base = test[0];
let sum = 0;
let tempK = 0;
let sort = 0;
let tempOutside = 0;
let tempInside = 0;
let baseIndex = 0;
if (k > test.length) {
return -1;
}
for (let index = 1; index < test.length; index++) {
const element = test[index];
if (element > base) {
tempOutside = test[index];
test[index] = base;
test[baseIndex] = tempOutside;
for (let j = index - 1; j >= 0; j--) {
if (element > test[j]) {
tempInside = test[j];
test[j] = test[j + 1];
test[j + 1] = tempInside;
}
}
} else {
base = test[index];
}
baseIndex++;
}
while (tempK < k) {
sum += test[sort];
if (tempK === k - 1 && sum % 2 !== 0) {
sum -= test[tempK];
tempK--;
}
tempK++;
sort++;
}
return sum;
};