Jenkins JFR Plugin

        Jenkins JFR 插件主要是用来解析JRockit Flight Record (不了解JRockit 飞行日志可以google一下),并且以SVG图形展示CPU,MEM等情况。想进一步了解代码的可以戳这里:

https://github.com/WalseWu/jenkins-jfr

      

        言归正传,回到hudson plugin,关于hudson的介绍就戳这里: http://wiki.eclipse.org/The_Hudson_Book 。简述下:hudson的插件机制主要基于Stapler和Jelly实现,Stapler主要用于将Hudson Classes绑定到URLs,举个简单的例子: 比如有个方法Hudson.getJob(String jobName),那么URL /job/foo/ 将会和 Hudson.getJob("foo")的返回值对象(应该是一个Project对象)绑定。Jelly可以理解为类似jsp和jstl;hudson通过jelly和stapler展示页面,和获取model对象。既然是插件机制,hudson提供了很多扩展点,详细请戳这里:http://wiki.hudson-ci.org/display/HUDSON/Extension+points。下面从扩展点入手,来介绍下Jenkins JFR 插件的开发:

1. Recorder

        该扩展点(hudson.ExtensionPoint)是一种Publisher(hudson.tasks.Publisher),其类图我就不画了(其实是OEL系统上木有装画图工具)。看javadoc:

Recorder is a kind of Publisher that collects statistics from the build, and can mark builds as unstable/failure. This marking ensures that builds are marked accordingly before notifications are sent via Notifiers. Otherwise, if the build is marked failed after some notifications are sent, inconsistency ensues.

To register a custom Publisher from a plugin, put Extension on your descriptor.

 意思表述的很明确,我用代码来解释吧。

 

  1)扩展Recorder,我写一个JFRPublisher作为整个插件的入口,类的架子大概如下:

public class JFRPublisher extends Recorder
{
        @Extension
	public static class DescriptorImpl extends BuildStepDescriptor<Publishe>
	{
		@Override
		public String getDisplayName()
		{
			return Messages.Publisher_DisplayName();
		}
		@Override
		public String getHelpFile()
		{
			return "/plugin/hudson-jfr/help.html";
		}
		public List<JFRReportParserDescriptor> getParserDescriptors()
		{
			return JFRReportParserDescriptor.all();
		}
		@Override
		public boolean isApplicable(@SuppressWarnings("rawtypes") Class<? extends AbstractProject> jobType)
		{
			return true;
		}
	}
       	private List<JFRReportParser> parsers;
	@DataBoundConstructor
	public JFRPublisher(List<? extends JFRReportParser> parsers)
	{
		if (parsers == null) {
			parsers = Collections.emptyList();
		}
		this.parsers = new ArrayList<JFRReportParser>(parsers);
	}
        @Override
	public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException,
			IOException
	{
                // .......暂时省略具体代码
         }
        //省略具体代码
}

          现在来解释前面的javadoc的描述,首先看最后一句注册一个custom publisher可以简单理解为代码中@Extension public static class DescriptorImpl extends BuildStepDescriptor<Publisher>,该类的作用是将JFRPubliusher注册为一个Publisher,于是在hudson配置页面会去查找hudson.plugins.jfr.JFRPublisher.config.jelly,用以显示页面,这里的的jelly是这样的:

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
	xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry 
      field="parsers">
    <f:hetero-list name="parsers" hasHeader="true"
                   descriptors="${descriptor.getParserDescriptors()}"
                   items="${instance.parsers}"
                   addCaption="${%Add a new report}"/>
  </f:entry>
</j:jelly>

          这是一个hetero-list,descriptors="${descriptor.getParserDescriptors()}"的值就是DescriptorImpl。getDisplayName()的返回值(这里是“Publish JFR result report”),items="${instance.parsers}"就是JFRPublisher的属性private List<JFRReportParser> parsers。先看下页面显示效果是这样的:


     当选中了Publish JFR result repot后,就会弹出一个parses(来自
private List<JFRReportParser> parsers)的下拉框提供选择(标题Add a new report来自上面jelly中addCaption设置),选择JFR Report就显示页面如下:



 

 2)ExtensionPoint

     该页面来自何处呢?显然来自JFRReportParser,因为上面下拉框中的每一项都是一个JFRReportParser对象。看代码:

 

public class JFRReportParser implements Describable<JFRReportParser>, ExtensionPoint
{
     @Extension
	public static class DescriptorImpl extends JFRReportParserDescriptor
	{
		@Override
		public String getDisplayName()
		{
			return "JFRReport";
		}
	}
  
       /**
	 * All registered implementations.
	 */
	public static ExtensionList<JFRReportParser> all()
	{
		return Hudson.getInstance().getExtensionList(JFRReportParser.class);
	}

        /**
	 * GLOB patterns that specify the jfr report.
	 */
	public final String glob;

	public final String title;

	public final int width;

	public final int height;

	/**
	 * JRockit Flight Recording events values which will be show on the graphs.
	 */
	public final String jfrEventSettingStr;
       @DataBoundConstructor
	public JFRReportParser(String glob, String jfrEventSettingStr, String title, int width, int height)
	{
		this.glob = glob == null || glob.length() == 0 ? getDefaultGlobPattern() : glob;
		this.jfrEventSettingStr = jfrEventSettingStr == null || jfrEventSettingStr.length() == 0 ? getDefaultJFREventsPattern()
				: jfrEventSettingStr.trim();
		resetDisplayName();
		this.title = title == null || title.length() == 0 ? "" : title;
		this.width = width <= 0 ? DEFAULT_WIDTH : width;
		this.height = height <= 0 ? DEFAULT_HEIGHT : height;
	}
    //........省略JFR event parse的逻辑,有兴趣可以 checkout git 源码:https://github.com/WalseWu/jenkins-jfr
}

 

           虽然扩展点不是Recoder,而是直接扩展Describable<JFRReportParser>, ExtensionPoint,但是和上面的publisher也大同小异,每一项的配置都会对应一个类中的属性,通过@DataBoundConstructor从页面提交中获取设置到变量中,对应jelly如下:

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
	xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <f:entry title="${%Report files}" field="glob">
    <f:textbox />
  </f:entry>
  <f:entry title="${%JFR Events}" field="jfrEventSettingStr">
    <f:textbox />
  </f:entry>
  <f:entry title="${%Graph Title:}" help="/plugin/hudson-jfr/graph-help.html">
    	<table width="100%">
    		<tr width="100%">
    			<td width="488">
    				<f:textbox field="title" width="488"/>
    			</td>
    			<td width="10"></td>
    			<td style="vertical-align:middle">${%Width}:</td>
    			<td>
    				<f:textbox field="width" width="100"/>
    			</td>
    			<td width="10"></td>
    			<td style="vertical-align:middle">${%Height}:</td>
    			<td>
    				<f:textbox field="height" width="100"/>
    			</td>
    		</tr>
    	</table>
  </f:entry>
</j:jelly>

 

3)BuildStep

    其实recorder本身就是BuildStep的实现类,该接口主要定义了一些生命周期函数,如prebuild,perform. perform方法可以用来添加一下action以便一些report保存到build中,著名的junit plugin就是这么做的。我们也类似:

 

//JFRPublisher
@Override
	public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException,
			IOException
	{
		PrintStream logger = listener.getLogger();

		// add the report to the build object.
		JFRBuildAction buildAction = new JFRBuildAction(build, logger, parsers);
		build.addAction(buildAction);

		for (JFRReportParser parser : parsers) {
			String glob = parser.glob;
			logger.println("JFR: Recording " + parser.getReportName() + " reports '" + glob + "'");

			List<FilePath> files = locateJFRReports(build.getWorkspace(), glob, logger);

			if (files.isEmpty()) {
				// build.setResult(Result.FAILURE);
				logger.println("JFR: no " + parser.getReportName() + " files matching '" + glob
						+ "' have been found. Has the report generated?. Setting Build to " + build.getResult());
				return true;
			}
			copyReportsToMaster(build, logger, files, parser.getDisplayName());
		}

		return true;
	}

 

     上面代码,每次build完,我创建了一个JFRBuildAction并添加到build中,这样buildAction就可以做他该做的事情(本插件当然是去解析展示JFR Report了);然后我遍历jenkins配置的parsers,根据配置去相应的jfr report 文件拷贝到每个build自己统一房jfr的地方。

 

4)StaplerProxy,Action

      下面是JFRBuilgAction:

 

public class JFRBuildAction implements Action, StaplerProxy{

    private final AbstractBuild<?, ?> build;
	private final List<JFRReportParser> parsers;

	private transient final PrintStream hudsonConsoleWriter;

	private transient WeakReference<JFRReportMap> jfrReportMap;

	private static final Logger logger = Logger.getLogger(JFRBuildAction.class.getName());

	public JFRBuildAction(AbstractBuild<?, ?> pBuild, PrintStream logger, List<JFRReportParser> parsers)
	{
		build = pBuild;
		hudsonConsoleWriter = logger;
		this.parsers = parsers;
	}
    public JFRReportParser getParserByDisplayName(String displayName)
	{
		if (parsers != null) {
			for (JFRReportParser parser : parsers) {
				if (parser.getDisplayName().equals(displayName)) {
					return parser;
				}
			}
		}
		return null;
	}

       public Object getTarget()
	{
		return getJfrReportMap();
	}

	public String getUrlName()
	{
		return "JFRReport";
	}

       private JFRReportMap getJfrReportMap()
	{
		JFRReportMap reportMap = null;
		WeakReference<JFRReportMap> wr = jfrReportMap;
		if (wr != null) {
			reportMap = wr.get();
			if (reportMap != null) {
				return reportMap;
			}
		}
		try {
			reportMap = new JFRReportMap(this, new StreamTaskListener(System.err, Computer.currentComputer().getDefaultCharset()));
		}
		catch (IOException e) {
			logger.log(Level.SEVERE, "Error creating new JFRReportMap()", e);
		}
		jfrReportMap = new WeakReference<JFRReportMap>(reportMap);
		return reportMap;
	}
//。。。省略部分代码
}

 重要的几个点:

a) getDisplayName返回值(我们config中会返回JFR Report)会在每个build页面的左侧生成一个link

b) 点击这个link会跳转到 由方法getTarget()返回值对应的地方, 所以我们接下来要看JFRReportMap

 

5)an ModelObject JFRReportMap

    上面点了link跳转到JFRReportMap到底是什么页面呢?先要看hudson.plugins.jfr.JFRReportMap.index.jelly

 

<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define"
	xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
  <l:layout xmlns:jm="/hudson/plugins/jfr/tags" css="/plugin/jfr-plugin/css/style.css">
  <st:include it="${it.build}" page="sidepanel.jelly" />
    <l:main-panel>
    	<h1>JFR Report</h1>
         <j:forEach var="jfrReport" items="${it.getJFRListOrdered()}">
         	<a href="./jfrEventTimeGraph?height=-1&amp;width=-1&amp;jfrReportPosition=${jfrReport.getReportFileName()}" 
         		target="_blank" title="${%Click for larger image}">
            	<h2>${jfrReport.getReportFileName()}</h2>
            </a>
    		<object data="./jfrEventTimeGraph?jfrReportPosition=${jfrReport.getReportFileName()}" width="${jfrReport.getWidth()}" height="${jfrReport.getHeight()}" />
        </j:forEach>
    </l:main-panel>
  </l:layout>
</j:jelly>

     解读下,主要是遍历jfrReportMap.getJFRListOrdered(),每个JFRReport会生成一个<a href=... />和一个<object data="..." />,什么意思呢?其实url是./jfrEventTimeGraph?,后面是参数,这个url对应一个方法jfrReportMap。doJfrEventTimeGraph(StaplerRequest,StaplerResponse),这就是Staplar的作用。看代码:

 

//in JFRReportMap.java
public void doJfrEventTimeGraph(StaplerRequest request, StaplerResponse response) throws IOException
	{
		try {
			String parameter = request.getParameter("jfrReportPosition");
			JFreeChart chart = GraphHelper.createOverlappingJFREventChart(getJFRReport(parameter));
			GraphHelper.exportChartAsSVG(chart, getJFRReport(parameter).getWidth(), getJFRReport(parameter).getHeight(), request,
					response);
		}
		catch (Exception e) {
			String newline = System.getProperty("line.separator");
			//Set exception message back for the failed test.
			StringWriter sb = new StringWriter(2048);
			sb.append(e.getMessage());
			sb.append(newline);
			e.printStackTrace(new PrintWriter(sb, true));
			logger.info(sb.toString());
		}
	}

public List<JFRReport> getJFRListOrdered()
	{
		List<JFRReport> listJFR = new ArrayList<JFRReport>(getJFRReportMap().values());
		logger.info("Ordered JFR Reports:" + listJFR.size());
		Collections.sort(listJFR);
		return listJFR;
	}

 

     这样SVG图就展示了, 大图和小图的区别是url后面的参数不一样。

到这里jenkins相关的都介绍完了,还剩下具体report的解析:

 

//JFRReportMap.java 在构造时就解析jfr了
/**
	 * Parses the reports and build a {@link JFRReportMap}.
	 * 
	 * @throws IOException
	 *             If a report fails to parse.
	 */
	JFRReportMap(final JFRBuildAction buildAction, TaskListener listener) throws IOException
	{
		this.buildAction = buildAction;
		parseReports(getBuild(), listener, null);
	}

private void parseReports(AbstractBuild<?, ?> build, TaskListener listener, final String filename) throws IOException
	{
		File repo = new File(build.getRootDir(), JFRReportMap.getJFRReportDirRelativePath());
		File[] dirs = repo.listFiles(new FileFilter() {
			public boolean accept(File f)
			{
				return f.isDirectory();
			}
		});
		// this may fail, if the build itself failed, we need to recover
		// gracefully
		if (dirs != null) {
			for (File dir : dirs) {
				JFRReportParser p = buildAction.getParserByDisplayName(dir.getName());
				if (p != null) {
					File[] listFiles = dir.listFiles(new FilenameFilter() {

						public boolean accept(File dir, String name)
						{
							if (filename == null) {
								return true;
							}
							if (name.equals(filename)) {
								return true;
							}
							return false;
						}
					});
					addAll(p.parse(build, Arrays.asList(listFiles), listener));
				}
			}
		}
	}

//下面是JFRReportParser。parse
public Collection<JFRReport> parse(AbstractBuild<?, ?> build, Collection<File> reports, TaskListener listener)
			throws IOException
	{
		Set<FileGroup> groupReports = groupFiles(reports);
		List<JFRReport> result = new LinkedList<JFRReport>();

		for (FileGroup fg : groupReports) {
			JFRReport r = new JFRReport(width, height);
			r.setGlobalName(glob, fg, getTitle());
			parseEventsReport(fg, r, getJfrEventSettings());
			result.add(r);
		}
		Collections.sort(result);
		return result;
	}

    

     具体jfr report的结构设计见下面类图,代码就不占篇幅了,有兴趣的可以去check out code: https://github.com/WalseWu/jenkins-jfr

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值