当前位置: 首页 > news >正文

Kepler性能分析之M2E调优

Author:Cai Eric

 

简介

    在使用Eclipse Kepler v4.3.2(WTP v3.5.2, M2E v1.4.1)开发基于Maven的Java Web项目时,将工程导入到workspace速度非常之慢,而且保存修改的文件也会引发长时间的编译,严重影响了开发人员的工作效率。

    通过分析日志,调试,辅以性能分析工具VisualVM, 发现了包括M2E-APT, WTP,M2E的诸多问题,并提交了相应的Patch, 其中有一些已经贡献回开源社区。

    本文将介绍对M2E的性能分析,下一篇,将侧重于WTP Validation和M2E-APT的性能优化。

测试

    我们的项目共包括11个工程,具备一定的复杂度。 我们选择了Eclipse Kepler和Juno,分别将项目导入到Workspace,计算Build和Validation全部结束的时间。

    测试结果显示,WTP3.5.2存在明显的性能瓶颈,同时Validation多次导入时间波动较大,可能存在某种不确定因素。

    另外,将项目导入到Eclipse过程中,进度条长时间停在“Maven Importing Projects”,这说明M2E在处理Maven项目时存在一定的性能问题。

    所以,应该把性能调优的重点放在WTP和M2E。

 Build        Build & Validation
Eclipse Kepler
(M2E 
1.4.1 WTP 3.5.2
5m50s13m-23m
Eclipse Juno
(M2E 
1.3.1 WTP 3.4.2
6m7m-16m

什么是M2E

    Maven已经是主流的构建工具,其build模型和Eclipse的build模型大相径庭,如何让Eclipse识别并准确地编译Maven项目?两者之间必然存在一种适配的过程。Maven IDE(简称M2E)就是为了解决这个问题开发的Eclipse插件, 它主要负责以下事情:

  • 分析Maven工程的依赖,设置成Java工程的classpath。
  • 由Maven builder执行定义在pom.xml中的各个maven 插件。

分析Maven工程的依赖

    M2E维护了自己的Maven工程模型,并提供了刷新API来管理。当pom.xml初次读取或者相关文件变更时,M2E会进行一次刷新来形成或同步模型。刷新主要是获取workspace工程间的依赖关系,解析工程各自的库依赖。刷新多是阻塞的,后续操作必须等待刷新完成。刷新是传递的,如果某工程的pom改变,该工程及依赖它的工程都需要重新刷新。

执行Maven插件

    M2E在导入或更新工程的时候,根据pom中的maven plugin定义,依次根据以下配置来匹配maven plugin的执行方式,形成生命周期配置(LifecycleMapping)。如下图所示。

  • pom: pom.xml(包括parent)的配置
  • extension:已安装的M2E Eclipse插件扩展
  • maven-plugin:打包在maven-plugin中的配置
  • default:M2E自带的常见maven-plugin缺省配置
  • uninteresting:pluin定义在M2E不感兴趣的phase(clean,package,install…)

执行方式包括:

  • ignore:不执行
  • configurator:调用M2E扩展的project configurator
  • execute:根据maven-plugin配置在完全或增量构建时执行指定的goal
  • error:不是执行方式,表示未找到任何配置

    Maven builder 发生在Java builder之后,执行时根据上述生命周期配置依次执行相关maven plugin. 这个过程是单线程执行的。 

性能分析及解决方案

不必要的重复刷新

问题

     在导入工程的时候,从日志中注意到,每个工程被刷新了3-4次,而且刷新工程非常耗时,对11个工程做一个同步的刷新需要50s左右。如果只做一次刷新,理论上来讲可以省掉2分钟左右的时间。

分析

    在M2E刷新的方法设置断点,分析得出每次刷新的原因:

  • ProjectFacade实例尚未创建,需要一次必要的刷新。
  • 后台Job刷新。 引入异步刷新的目的是在自动构建未启用的情况下,当工程打开/新建时,后台需做相应刷新, 但实际在自动构建启用时,导入工程,后台Job仍然做了一次刷新,该刷新应可避免。当然由于异步执行,这部分时间不会缩减太多。
  • .classpath 和.project生成或重新生成, M2E认为模型过时需刷新。生成是指将工程第一次导入workspace时,.classpath和.project初次生成;重新生成是指在第二次导入时,上述文件已经生成,但M2E还是会重新覆写。这些文件由M2E的Java工程配置器生成的。
  • M2E-WTP需要为某个output classpathEntry设置一个WTP特需的属性,因而修改了.classpath, M2E认为模型过时再次刷新。 这次刷新仅针对个别web工程。 M2E-WTP是M2E和WTP两大Eclipse项目间的桥接, 用于支持基于Maven的JavaEE工程开发。
     
方案

    M2E的刷新其实是一个对POM定义的理解和转译过程,这个过程应该是单向的, M2E解析 pom.xml生成.classpath和.project, 那么为什么要在.classpth和.project改变后再次刷新呢?我们可以考虑让M2E的刷新取消关注.classpth和.project的变更,或者如果必须应对这些文件的变更,我们是否可以引入某些cache 来实现模型的部分刷新(即缓存多次刷新时模型中保持不变的部分)?

    将上述分析提交给了M2E社区之后,得到了项目负责人的重点关注,他们同意了第一种方案。我们贡献了以下patch,应用以下patch可减少2次同步刷新,1次异步刷新,这样就保证每个工程在导入时仅需进行一次刷新。这些patch将在M2E 1.6.0 生效。
 

描述

Bug

取消 .classpath or .project改变时的刷新

https://bugs.eclipse.org/bugs/show_bug.cgi?id=436668

避免导入工程的二次刷新

https://bugs.eclipse.org/bugs/show_bug.cgi?id=436679

避免classpath container initialization的二次刷新

https://bugs.eclipse.org/bugs/show_bug.cgi?id=437493
 

测试

 

Build

Build & Validation

Eclipse Kepler
(M2E 
1.4.1 WTP 3.5.2

3m47s

N/A

    测试结果显示省下的时间大概在2分钟,约提升30%。

并发刷新

问题

    M2E会根据工程间依赖的顺序来依次刷新每个maven 工程。从后台日志看,这个刷新是串行的,并且耗时不少。

Line 11233: 13:14:11.368 [Worker-174] DEBUG o.e.m.c.internal.embedder.MavenImpl - Read Maven project: ***\pom.xml in 4555 ms
  …
Line 11257: 13:14:20.916 [Worker-174] DEBUG o.e.m.c.internal.embedder.MavenImpl - Read Maven project: ***\pom.xml in 889 ms
Line 11273: 13:14:45.190 [Worker-174] DEBUG o.e.m.c.internal.embedder.MavenImpl - Read Maven project: ***\pom.xml in 20530 ms
分析

    刷新分为两个阶段,第一阶段是读取pom.xml并建立起workspace现有工程间的相互依赖关系,这个过程很快。第二阶段是深度解析每个工程的直接或间接依赖,这个过程主要是由M2E调用Maven处理,后者根据pom定义从本地maven repo匹配或从remote repo下载依赖所需的artifacts (Jar, pom等等)。这个阶段非常耗时,实际耗费的时间取决于工程的复杂度。在本地maven repo已经有全部缓存的情况下,刷新我们的工程所需时间在1s-23s之间。前面提及一次同步的刷新需要50s左右,如果将这一过程并发,刷新的时间基本等同于最耗时的工程,理论上可以减少半分钟。

方案

    改写了M2E第二阶段的刷新逻辑,将调用Maven API深度解析依赖的这个过程并发执行。以下是并发实现还需注意的问题:
    M2E可以将依赖解析为workspace artifact,即在工程尚未做一次mvn install的时候,workspace工程的artifact在本地maven repo是不存在的,M2E会将之解析为workspace中的工程,在工程的classpath体现为工程间的依赖,而不是jar library的依赖。在并发刷新时,由于workspace artifact尚未初始化,maven会尝试从remote repo下载,下载失败后把这些依赖标为缺失。在并发刷新结束后,检查标为缺失的依赖,重新替换为对应的workspace artifact。

    在刷新一个工程时,maven会首先解析出该工程的所有依赖(artifact),然后启用多个(缺省为5个)线程去下载本地不存在的artifacts,每个artifact需要下载多个文件,包括jar,pom, maven-metadata-snapshots.xml(Snapshot版本所需)等,这些文件之间存在一定的联系,需要确保由同一个线程来依次下载。 在多个工程并发刷新的情况下,工程的依赖多有重复,同一个artifact有可能会并发或重复下载,比如当一个线程下载了某个artifact的pom文件时,另一个线程下载了maven-metadata-snapshots.xml,解析就会出问题,因此需要对Maven的ArtifactResolver加锁来确保同一个artifact的多个文件在同一个线程完成下载。这里我们把依赖的下载串行化, 而依赖的解析仍然是并发的,实际测试发现,加锁确保依赖的串行下载并不会带来明显的性能损耗。相当于在刷新的第二阶段,我们提高了依赖的解析,但没有提高下载的速度。

    Maven的对象管理基于一个名为Plexus的反转控制容器,可以很方便地通过注册我们的实现来覆写既有的ArtifactResolver实现。我们计划把这个patch提交给m2e社区。

测试

 

Build

Build & Validation

Eclipse Kepler
(M2E 
1.4.1 WTP 3.5.2

3m20s

N/A

    测试结果显示省下的时间为27s。如果你的工程依赖复杂性大致相同,则可以达到最优。

maven-plugin二次执行

问题

    在M2E的控制台输出中,发现我们定制的多个maven-plugin都被执行了两次,并且每次执行的时间基本相同。每个工程导入到Workspace的过程中应该只build一次,为什么我们的maven-plugin会被执行两次呢?

分析

    前面提及M2E执行maven-plugin,或忽略,或调用M2E扩展实现,或执行maven-plugin的特定goal。我们的maven-plugin正是通过第三种方式参与Eclipse构建的。将M2E所需的配置文件lifecycle-mapping-metadata.xml,打包至META-INF/m2e下,配置示例:

<lifecycleMappingMetadata>
  <pluginExecutions>
    <pluginExecution>
      <pluginExecutionFilter>
        <goals>
          <goal>some-goal</goal>
        </goals>
      </pluginExecutionFilter>
      <action>
        <execute>
          <runOnIncremental>default false </runOnIncremental>
          <runOnConfiguration>default false</runOnConfiguration>
        </execute>
      </action>
    </pluginExecution>
  </pluginExecutions>
</lifecycleMappingMetadata>
  • runOnIncremental缺省为false,仅参与完全构建;在设置为true时,参与增量构建和完全构建。
  • runOnConfiguration缺省为false,设置为true时,参与maven project update. 比如有些plugin会创建用于生成代码的source目录,需要更新工程配置。

    更多信息可参考:http://wiki.eclipse.org/M2E_plugin_execution_not_covered

    当Maven builder在编译工程时,会根据phase依次调用每个maven plugin,每个plugin执行后将变更的资源知会M2E, M2E 随后刷新资源,刷新资源会再次引发增量构建,M2E 随后依次调用每个参与增量构建的maven plugin,如又有资源变更,再次增量编译… 这意味着在导入工程的过程中,一个工程的完全构建会紧接着一个或多个增量构建,如果maven plugin同时参与完全构建和增量构建,该maven plugin至少会被执行两次,如果maven builder不能在增量构建中正确结束,就会进入死循环。 M2E为了解决上述问题,提出了maven-plugin的实现规范,要求实现者分析变更的资源,仅在必要的时候执行,并且把变更的资源返回。请参阅:http://wiki.eclipse.org/M2E_compatible_maven_plugins

    由于我们的一个maven plugin需要生成某些必需文件,如果未生成,应用部署就会失败。为了确保文件生成,只能参与到增量构建。 这个原因其实是Java builder在某些情况下(比如classpath change,classpath error,Out-of-sync files under target/classes等),会从增量构建自动切换到完全构建,而完全构建会把output目录(比如target/classes)清空。比如,当我们改变一个文件并保存,增量构建发生,Java builder因为某种原因切换到完全构建,output目录清空,重新编译Java文件,随后maven builder开始执行,但它并不知道这种切换,继续进行原来的增量构建,依次调用参与增量构建(如上述配置所示)的maven plugin。如果我们的maven plugin未参与增量构建,就不会被maven builder调用,而此时output目录下之前生成的那些必需文件已经被java builder删除,当用户起server部署就会失败。

方案     

    检查了maven plugin的实现,若无必要参与增量构建,则在maven plugin的配置中声明。如果必须参与增量构建,那么两次执行就无法避免,可以考虑让后面的一次执行尽快结束。修改实现,通过检查部署文件是否已经生成,若存在则不再执行,这样的效果基本与执行一次的效果相当。

测试

 

Build

Build & Validation

Eclipse Kepler
(M2E 
1.4.1 WTP 3.5.2

2m50s

N/A

    测试结果显示省下的时间为30s左右,可见maven plugin的编写不仅要合乎规范,还要性能优先。

结论

    通过以上优化后,将工程导入到Eclipse从5m50s缩减到2m50s,性能提升了51%。下一步,可以考虑将M2E和JDT更多的事情并发化: 比如工程的编译, 同一阶段的maven-plugin的执行,Artifact下载,另外M2E仰赖Maven 的Aether API来分析依赖, 如果这部分逻辑可以优化,则可以带来立竿见影的效果,事实上Maven的Aether API正是在不断改进中。

相关文章:

  • Ebay开源 Pulsar:实时大数据分析平台
  • JS组件化验证检测
  • 基于云技术的集成测试代码覆盖率收集的一站式解决方案
  • 使用github pages + issues + api建立个人博客
  • MapReduce的详细过程
  • 基于Jmeter和Jenkins的自动化性能测试的一站式解决方案
  • jQuery动态载入JS文件研究
  • SolrCloud之分布式索引及与Zookeeper的集成
  • Kafka的分布式架构设计与High Availability机制
  • JS方法代理
  • Hadoop作业性能指标及参数调优实例 (一)Hadoop作业性能异常指标
  • Hadoop作业性能指标及参数调优实例 (二)Hadoop作业性能调优7个建议
  • Hadoop作业性能指标及参数调优实例 (三)Hadoop作业性能参数调优方法
  • 漫谈程序控制流
  • Hadoop集群硬盘故障分析与自动化修复
  • .pyc 想到的一些问题
  • CSS相对定位
  • emacs初体验
  • Flannel解读
  • golang 发送GET和POST示例
  • Joomla 2.x, 3.x useful code cheatsheet
  • Js基础知识(四) - js运行原理与机制
  • LeetCode29.两数相除 JavaScript
  • orm2 中文文档 3.1 模型属性
  • python_bomb----数据类型总结
  • React中的“虫洞”——Context
  • SpriteKit 技巧之添加背景图片
  • Webpack入门之遇到的那些坑,系列示例Demo
  • webpack项目中使用grunt监听文件变动自动打包编译
  • 百度小程序遇到的问题
  • 欢迎参加第二届中国游戏开发者大会
  • 记录:CentOS7.2配置LNMP环境记录
  • 前端之React实战:创建跨平台的项目架构
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 提醒我喝水chrome插件开发指南
  • 我的zsh配置, 2019最新方案
  • ​一帧图像的Android之旅 :应用的首个绘制请求
  • #《AI中文版》V3 第 1 章 概述
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • ( 10 )MySQL中的外键
  • (c语言版)滑动窗口 给定一个字符串,只包含字母和数字,按要求找出字符串中的最长(连续)子串的长度
  • (ZT) 理解系统底层的概念是多么重要(by趋势科技邹飞)
  • (ZT)一个美国文科博士的YardLife
  • (二)c52学习之旅-简单了解单片机
  • (二)fiber的基本认识
  • (二十四)Flask之flask-session组件
  • (转) ns2/nam与nam实现相关的文件
  • (转)创业家杂志:UCWEB天使第一步
  • (转)使用VMware vSphere标准交换机设置网络连接
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .net core 实现redis分片_基于 Redis 的分布式任务调度框架 earth-frost
  • .Net CoreRabbitMQ消息存储可靠机制
  • .net 打包工具_pyinstaller打包的exe太大?你需要站在巨人的肩膀上-VC++才是王道
  • .NET 反射的使用
  • .net反编译的九款神器