Skip to content

Adding nonstandard support for c:set and c:remove#842

Merged
jengebr merged 1 commit into
apache:mainfrom
jengebr:nonstandard_tags
Apr 16, 2025
Merged

Adding nonstandard support for c:set and c:remove#842
jengebr merged 1 commit into
apache:mainfrom
jengebr:nonstandard_tags

Conversation

@jengebr

@jengebr jengebr commented Apr 4, 2025

Copy link
Copy Markdown
Contributor

Enables optimized, non-standard behavior for the most common JSP tags, when configured to do so.

Impact: benchmark (included)shows 99% reduction in cpu, 100% reduction in object allocation

Activation: off by default, enabled via an init param to the JSP servlet, such as:

      <init-param>
        <param-name>useNonstandardTagOptimizations</param-name>
        <param-value>c:set,c:remove</param-value>
      </init-param>

Example:
Old code (standard): one method, two parameters, 10+ lines of code, one object allocated (and it's large!).

  private boolean _jspx_meth_c_005fset_005f39(javax.servlet.jsp.tagext.JspTag _jspx_th_c_005fwhen_005f11, javax.servlet.jsp.PageContext _jspx_page_context)
          throws java.lang.Throwable {
    javax.servlet.jsp.PageContext pageContext = _jspx_page_context;
    javax.servlet.jsp.JspWriter out = _jspx_page_context.getOut();
    //  c:set
    org.apache.taglibs.standard.tag.rt.core.SetTag _jspx_th_c_005fset_005f39 = new org.apache.taglibs.standard.tag.rt.core.SetTag();
    _jsp_getInstanceManager().newInstance(_jspx_th_c_005fset_005f39);
    try {
      _jspx_th_c_005fset_005f39.setPageContext(_jspx_page_context);
      _jspx_th_c_005fset_005f39.setParent((javax.servlet.jsp.tagext.Tag) _jspx_th_c_005fwhen_005f11);
      // /WEB-INF/views/jsp/features/buybox/offerDisplayGroupLayout.jsp(230,12) name = var type = java.lang.String reqTime = false required = false fragment = false deferredValue = false expectedTypeName = null deferredMethod = false methodSignature = null
      _jspx_th_c_005fset_005f39.setVar("groupName");
      // /WEB-INF/views/jsp/features/buybox/offerDisplayGroupLayout.jsp(230,12) name = value type = javax.el.ValueExpression reqTime = true required = false fragment = false deferredValue = true expectedTypeName = java.lang.Object deferredMethod = false methodSignature = null
      _jspx_th_c_005fset_005f39.setValue(new org.apache.jasper.el.JspValueExpression("/WEB-INF/views/jsp/features/buybox/offerDisplayGroupLayout.jsp(230,12) '${tabContents.keySet().toArray()[0]}'",_jsp_getExpressionFactory().createValueExpression(_jspx_page_context.getELContext(),"${tabContents.keySet().toArray()[0]}",java.lang.Object.class)).getValue(_jspx_page_context.getELContext()));
      int _jspx_eval_c_005fset_005f39 = _jspx_th_c_005fset_005f39.doStartTag();
      if (_jspx_th_c_005fset_005f39.doEndTag() == javax.servlet.jsp.tagext.Tag.SKIP_PAGE) {
        return true;
      }
    } finally {
      org.apache.jasper.runtime.JspRuntimeLibrary.releaseTag(_jspx_th_c_005fset_005f39, _jsp_getInstanceManager(), false);
    }
    return false;
  }

New code (nonstandard) - one parameter, one line of code, zero objects allocated.

  private void _jspx_meth_c_005fset_005f0(jakarta.servlet.jsp.PageContext _jspx_page_context)
          throws java.lang.Throwable {
    //  c:set
    // /jsp/generator/nonstandard/set-01.jsp(18,0) name = var type = java.lang.String reqTime = false required = false fragment = false deferredValue = false expectedTypeName = null deferredMethod = false methodSignature = null
    // /jsp/generator/nonstandard/set-01.jsp(18,0) name = value type = jakarta.el.ValueExpression reqTime = true required = false fragment = false deferredValue = true expectedTypeName = java.lang.Object deferredMethod = false methodSignature = null
    org.apache.jasper.runtime.JspRuntimeLibrary.nonstandardSetTag(_jspx_page_context, "testVar", new org.apache.jasper.el.JspValueExpression("/jsp/generator/nonstandard/set-01.jsp(18,0) '${'testValue'}'",_jsp_getExpressionFactory().createValueExpression(_jspx_page_context.getELContext(),"${'testValue'}",java.lang.Object.class)).getValue(_jspx_page_context.getELContext()), jakarta.servlet.jsp.PageContext.PAGE_SCOPE);
  }

Caller code is simplified by eliminating the boolean check, from:

          if (_jspx_meth_c_005fset_005f39(_jspx_th_c_005fwhen_005f11, _jspx_page_context))
            return true;

to

        _jspx_meth_c_005fset_005f125(_jspx_page_context);

@funkman

funkman commented Apr 10, 2025

Copy link
Copy Markdown

While only targeting c:set/c:remove - Do you also need to check the tag belongs to <uri>http://java.sun.com/jsp/jstl/core</uri> ? I suspect the user wouldn't enable the flag then use a different library with the same tag names, but it seems like a possible weird outcome

@jengebr

jengebr commented Apr 10, 2025

Copy link
Copy Markdown
Contributor Author

While only targeting c:set/c:remove - Do you also need to check the tag belongs to <uri>http://java.sun.com/jsp/jstl/core</uri> ? I suspect the user wouldn't enable the flag then use a different library with the same tag names, but it seems like a possible weird outcome

Great suggestion, thank you. Edge cases are a big concern so please keep them coming!

@jengebr
jengebr marked this pull request as ready for review April 11, 2025 19:30

@markt-asf markt-asf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking long term, I am wondering whether we need to provide the ability for users to customise these optimisations. If we do then we can add something like a GeneratorFactory and users can provide their own generator that overrides the built-in one along the lines of ELInterpreterFactory. For now, I don't think is necessary.

Comment on lines +960 to +968

public static void nonstandardSetTag(jakarta.servlet.jsp.PageContext pageContext, String var, Object value) {
if (value == null) {
pageContext.removeAttribute(var);
} else {
pageContext.setAttribute(var, value);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only appears to be used in test code. If true, it should be removed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated JSP code calls this method, why is why it's not called within Tomcat itself.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you point out where. I only see calls to the 4-arg method, not the 3-arg method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, sorry, I missed the 3-arg vs. 4-arg distinction. You're correct and I'll remove the 3-arg.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I verified this by comparison to the JSTL tag and realized I missed some logic to clean up the VariableMapper. I added that as well.

Comment on lines +3213 to +3242
if (scope == null) {
out.printil("pageContext.removeAttribute(\"" + varValue + "\");");
} else {
String scopeValue = translateScopeToConstant(jspAttributes);
out.printil("pageContext.removeAttribute(\"" + varValue + "\", " + scopeValue + ");");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This takes a different approach to handling null scope compared to set. It isn't wrong but it is inconsistent. Why? If there is a good reason, it should be documented else folks coming later are going to wonder why. If there isn't a good reason, set and remove should be consistent (and I have no preference for either approach).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I modeled the code here after the tag implementations (SetTag and RemoveTag), with the goal of maintaining compatibility... so any mismatch is either an error in how I copied the logic, or how the tags themselves are implemented. What specific differences do you see?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set calls

String scopeValue = translateScopeToConstant(jspAttributes);

whereas remove treats scope == null as a special case

if (scope == null) {
  out.printil("pageContext.removeAttribute(\"" + varValue + "\");");
} else {
    String scopeValue = translateScopeToConstant(jspAttributes);
    out.printil("pageContext.removeAttribute(\"" + varValue + "\", " + scopeValue + ");");
}

I'd probably remove the special case treatment from remove.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird as this is, I think it matches the logic from RemoveTag:

    // removes the variable (from a specific scope, if specified)
    public int doEndTag() throws JspException {
        if (!scopeSpecified)
            pageContext.removeAttribute(var);
        else
            pageContext.removeAttribute(var, scope);
	return EVAL_PAGE;
    }

And PageContextImpl .remove(var) removes from every scope, whereas .remove(var, scope) only removes from one:.

    @Override
    public void removeAttribute(final String name) {

        if (name == null) {
            throw new NullPointerException(Localizer.getMessage("jsp.error.attribute.null_name"));
        }

        removeAttribute(name, PAGE_SCOPE);
        removeAttribute(name, REQUEST_SCOPE);
        if (session != null) {
            try {
                removeAttribute(name, SESSION_SCOPE);
            } catch (IllegalStateException ise) {
                // Session has been invalidated.
                // Ignore and fall throw to application scope.
            }
        }
        removeAttribute(name, APPLICATION_SCOPE);
    }

Until this effort, I didn't know that remove w/o scope cleared all scopes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. I didn't realise there was a difference until I looked at the PageContext Javadoc. It is probably worth a short comment on both set and remove on how no/null scope is handled since it is different.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great suggestion, will do. I'll make a pass on all of this code and add more descriptive comments wherever I got surprised.

@jengebr

jengebr commented Apr 15, 2025

Copy link
Copy Markdown
Contributor Author

Thinking long term, I am wondering whether we need to provide the ability for users to customise these optimisations. If we do then we can add something like a GeneratorFactory and users can provide their own generator that overrides the built-in one along the lines of ELInterpreterFactory.

Honestly, this is where I started, but I had to make a bazillion fields, methods, and classes public or protected. I figured I'd start here and we can bite that off later.

@markt-asf markt-asf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with GeneratorFactory being something we consider later if necessary. Just want to make sure we don't do anything now to make it harder to do that if we need to.

Comment on lines +960 to +968

public static void nonstandardSetTag(jakarta.servlet.jsp.PageContext pageContext, String var, Object value) {
if (value == null) {
pageContext.removeAttribute(var);
} else {
pageContext.setAttribute(var, value);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you point out where. I only see calls to the 4-arg method, not the 3-arg method.

Comment on lines +3213 to +3242
if (scope == null) {
out.printil("pageContext.removeAttribute(\"" + varValue + "\");");
} else {
String scopeValue = translateScopeToConstant(jspAttributes);
out.printil("pageContext.removeAttribute(\"" + varValue + "\", " + scopeValue + ");");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set calls

String scopeValue = translateScopeToConstant(jspAttributes);

whereas remove treats scope == null as a special case

if (scope == null) {
  out.printil("pageContext.removeAttribute(\"" + varValue + "\");");
} else {
    String scopeValue = translateScopeToConstant(jspAttributes);
    out.printil("pageContext.removeAttribute(\"" + varValue + "\", " + scopeValue + ");");
}

I'd probably remove the special case treatment from remove.

@jengebr
jengebr force-pushed the nonstandard_tags branch 4 times, most recently from 5279b3a to 8957f56 Compare April 16, 2025 19:26
@jengebr
jengebr merged commit 5ca117b into apache:main Apr 16, 2025
@jengebr
jengebr deleted the nonstandard_tags branch April 18, 2025 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants